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

Uploaded Source

Built Distributions

koerce-0.2.0-cp312-cp312-win_amd64.whl (425.3 kB view details)

Uploaded CPython 3.12 Windows x86-64

koerce-0.2.0-cp312-cp312-win32.whl (349.7 kB view details)

Uploaded CPython 3.12 Windows x86

koerce-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl (3.3 MB view details)

Uploaded CPython 3.12 musllinux: musl 1.2+ x86-64

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

Uploaded CPython 3.12 musllinux: musl 1.2+ i686

koerce-0.2.0-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.0-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.0-cp312-cp312-macosx_14_0_arm64.whl (482.1 kB view details)

Uploaded CPython 3.12 macOS 14.0+ ARM64

koerce-0.2.0-cp312-cp312-macosx_13_0_x86_64.whl (527.5 kB view details)

Uploaded CPython 3.12 macOS 13.0+ x86-64

koerce-0.2.0-cp311-cp311-win_amd64.whl (446.6 kB view details)

Uploaded CPython 3.11 Windows x86-64

koerce-0.2.0-cp311-cp311-win32.whl (363.3 kB view details)

Uploaded CPython 3.11 Windows x86

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

Uploaded CPython 3.11 musllinux: musl 1.2+ i686

koerce-0.2.0-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.0-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.0-cp311-cp311-macosx_14_0_arm64.whl (481.9 kB view details)

Uploaded CPython 3.11 macOS 14.0+ ARM64

koerce-0.2.0-cp311-cp311-macosx_13_0_x86_64.whl (543.8 kB view details)

Uploaded CPython 3.11 macOS 13.0+ x86-64

koerce-0.2.0-cp310-cp310-win_amd64.whl (442.1 kB view details)

Uploaded CPython 3.10 Windows x86-64

koerce-0.2.0-cp310-cp310-win32.whl (363.2 kB view details)

Uploaded CPython 3.10 Windows x86

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

Uploaded CPython 3.10 musllinux: musl 1.2+ i686

koerce-0.2.0-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.0-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.0-cp310-cp310-macosx_14_0_arm64.whl (481.1 kB view details)

Uploaded CPython 3.10 macOS 14.0+ ARM64

koerce-0.2.0-cp310-cp310-macosx_13_0_x86_64.whl (542.1 kB view details)

Uploaded CPython 3.10 macOS 13.0+ x86-64

File details

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

File metadata

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

File hashes

Hashes for koerce-0.2.0.tar.gz
Algorithm Hash digest
SHA256 28083f248c5dfab919ca58fd20514fd955a1767a3c6ae1d6ba88f08fbce4b1d3
MD5 37abcd1b6783b1397982339ed7c70ed1
BLAKE2b-256 ce540268273248bfca682a6b92fcbef50ec7517c2b494cd6548b89cfa0578033

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for koerce-0.2.0-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 94b6f3d0e8b06a51369760e0bab53a145e597e64a1e44d956e52f6c113db6745
MD5 14c11632f247fb7431a2f98291d7e4aa
BLAKE2b-256 292782f388514706c4e72ce1285c3f37e819b30455e2af39f5ee33458c318154

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for koerce-0.2.0-cp312-cp312-win32.whl
Algorithm Hash digest
SHA256 01b07c8565deee5d2729f4b3035c58de9be9b9d52066c11bff1bab60345d0dd2
MD5 fba92233f3aa39b90e2144c760172703
BLAKE2b-256 febb32e5c4e86e2c5d1d1bcca6760f6b3daa2b0edca74b6137b7230b6dcaf256

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 c96c4467a25d93a1f6a7447f852a510d396dffb9f10138348ec9a0e19571b228
MD5 a0922856e02a228445d00504b23b2c11
BLAKE2b-256 fbdfaa51537149697fb5c66e1c47b275f497a2fd797b9d6188298fd47ca72588

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp312-cp312-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 c14bf69f2d690ea7e9959053958c0bace4a697e25724485f6e1dbae690ffdd3c
MD5 dc7a97bdfa3334000ca676135f128321
BLAKE2b-256 118609407c81df0550883b8abf32bdc0533f8ea238e493a827521b378ed12657

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1ea5a1f5f81d37b40ecd19fccb6f71978f2b3a5df9375414aa93783e0d7bd698
MD5 b66ae096256110e53555044f68196875
BLAKE2b-256 7250ad831540b43f314819257dd261135571df0a5a88b23a4ef9901ba08283b3

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 9f67405e23d751e75b125df760edf0a5f476a17a02221c4f33805e06be44bd82
MD5 a5466227beac294ab6d18066239e0bb0
BLAKE2b-256 de4527dbb3d96869feff494706ac566e8864a0c5374bb1cf9bf0c51643ed8147

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp312-cp312-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 8a8f4f1bbc6f82f4e3b68dd063e5d1bec33d33d1939c3c1a02c8ddd59f495e0e
MD5 374831ae597035a56a6853e1c3319acc
BLAKE2b-256 25bdf5ac622df5a4289dcd33808112ef49c824f456b785a26054840dc1134a43

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp312-cp312-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 eaf16160c8a6fcc47f1830e1a4f16f028c0ef23991d96b7e8b3daddb501a140a
MD5 d8228b82921cc0b3077bef7a7241ce46
BLAKE2b-256 53c250b6734f2b3e5444d1e07f52194f474602435a664b46674b7f944e139670

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for koerce-0.2.0-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 2c3940e954811ec20d723d489d8b3b18ec11f0c6defe88e6b5499a19c83d7686
MD5 a27bc120156422d0c2b1b6090194c4e5
BLAKE2b-256 227ea6e0aacfaedbcbf186737073433a6783bdd3bd886bf5358143d88929b8ca

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for koerce-0.2.0-cp311-cp311-win32.whl
Algorithm Hash digest
SHA256 7cc834553af6517b58835a73e5165d93680abacc0fa02cd9703051a5eb875aee
MD5 773ba32622066ba8817690ef1d26ac2b
BLAKE2b-256 ed168b430f644d267a16b06660d0bb9f87c5c10a5972a498c3921323fc0d2a7d

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 06618cc8e4481d5d1ed51c18b68979649b52efb2c5b668ecca16f7f551f6bccd
MD5 69c181f4a182ed1c072bb18ba03e546e
BLAKE2b-256 7e994a69dac11b9e37e60e770d737af7165f2d3d48759a7a550edeee4dfff63b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp311-cp311-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 7d97809303adf8913980f7167e968a9a875697202a23868ecdab1c24952e49f9
MD5 6f470fa2e6ebaec674c313ff61c0243e
BLAKE2b-256 bdc601af5c5db856499aea423b2b64df8279ebc12744fe8cbade80f15b5d0476

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 3b08a7d507809482d76e9708b00d84a7beb9257887dc18bec554e096da70f915
MD5 68863ed9c58986f196f125fac63efd3d
BLAKE2b-256 be2f9de5d1276c738f848c94949637ac5642c242397bc80bd6dc47dd141965f6

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 9dcc40cc6ed795980356a2db5190ee62e44f64d9fd4d659d89391d6de4d3f94a
MD5 216c6769791f700175688e3b655665fe
BLAKE2b-256 dc0ae418720dff508855c2da37a3d92e69c5268c4dd582de06dbe7144469bf0b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp311-cp311-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 0a50a1471713e033718c0aa29605cdbdbc790cb116f79bf206ae249db1c528df
MD5 0af5743f3eb834c9bb6738886c4b0e58
BLAKE2b-256 c5832efbc2fd1b511dc7168fe86c7d147262beeebacbaff2668bd7d4e6523938

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp311-cp311-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 7489c9a01a30efa731d94744fca4914cb96efb67f3750eb4dfb07a18c44e9e5b
MD5 24d0c28dee147dbe2c071946afc10784
BLAKE2b-256 ee74d024848b3909862776a588b225df966065e61428ffcd1887dda3b3240f73

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for koerce-0.2.0-cp310-cp310-win_amd64.whl
Algorithm Hash digest
SHA256 b8283b8a8cde6a166bfb7873fd47105222a5c90342957601dc5b0a795ed14816
MD5 3683bf2efe483d4617fd93bc5bafcf70
BLAKE2b-256 16a7b0ace0d06a589972bd0f49f735991abe64a31190b9830d5c9e250a3f8c2b

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for koerce-0.2.0-cp310-cp310-win32.whl
Algorithm Hash digest
SHA256 e84f6e7dcdb2b7e011a0a87e89b4e44800519cf7ca72fb5e561ff11649c0d71b
MD5 7d38092c4765d00991ff21ad9e455186
BLAKE2b-256 c047b5e7e131aa2f73264f40337c0a96bfecf8ee61ff5a980319548bc0159196

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 1149b498d7dcfa9ba41209ff0d9ac0f30c2a5c8024ca0ee0357129b20e494dbd
MD5 5ba7786215613786b9306f096068c468
BLAKE2b-256 f7ee38089594c37b67c75ec0b403e2f7988bdca530cc3bb9e8eba9dc8b103add

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp310-cp310-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 78bb79bc5dc8c98326020856a96ed778b0e2bb561fc43d3a1be12dd7f6604a24
MD5 bfc1ebe6a57e7780d90326551f2f2ceb
BLAKE2b-256 61c999bfb5718a1ea1d7381936b603e8cfb1bda3320083c82af4d6900ac42229

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0747bbffdaccb7133f0d224d02161c82053237f5eb237a23d24a049d2de834b0
MD5 2c50e41c153b736316ac75d89f9b8d3f
BLAKE2b-256 9ab38db79646e75452fe5c739b3ef3843228dae85e2370c7727b0e1dd6f688fa

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 593865b58ff6d1f05e7edd75f40df15f44a2b55087e5ade0a2cbef94e3ff01b3
MD5 f1f34cffce170bbfb1912efbbcdd737c
BLAKE2b-256 8f3b064a0c8d20bba60ea0b7cac1fd37f36bc550e1861061a678642dae7f91c5

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp310-cp310-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 1ec41b82bb5d09b026e3d5d46300015015a945d73b9449d0e7481a725aa72787
MD5 2675831cc95b062c31888e7ca755dc4e
BLAKE2b-256 d2c83128482398ebb83238fd7d25c788ebbd834a651aff7654501c7db6d8f61f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.0-cp310-cp310-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 b1c76208a9dc623681759fa646404ede032fef5fddbba2e7b69215ac844173f4
MD5 ad42e55d24f5b28d1b91d6f8be6b9cf2
BLAKE2b-256 56da7e602ffbb386c5aeab52df8711c607403d2daa87d2e8cc65a718618885e2

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