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.
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}
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
Custom
Capture
Replace
SequenceOf
/ ListOf
/ TupleOf
MappingOf
/ DictOf
/ FrozenDictOf
PatternList
PatternMap
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:
- 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.
- 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:
- More advanced matching examples
- Add benchmarks against pydantic
- Show variable capturing
- Show match and replace in nested structures
- Example of validating functions by using @annotated decorator
- Explain
allow_coercible
flag - Mention other relevant libraries
Other examples
from koerce import match, NoMatch
from koerce.sugar import Namespace
from koerce.patterns import SomeOf, ListOf
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
assert match(int, 1) == 1
assert match(ListOf(int), [1, 2, 3]) == [1, 2, 3]
from dataclasses import dataclass
from koerce.sugar import match, Namespace, var
from koerce.patterns import pattern
from koerce.builder import builder
@dataclass
class A:
x: int
y: int
@dataclass
class B:
x: int
y: int
z: float
p = Namespace(pattern, __name__)
d = Namespace(builder, __name__)
x = var("x")
y = var("y")
assert match(p.A(+x, +y) >> d.B(x=x, y=1, z=y), A(1, 2)) == B(x=1, y=1, z=2)
More examples and a comprehensive readme are on the way.
Packages are not published to PyPI yet.
Python support follows https://numpy.org/neps/nep-0029-deprecation_policy.html
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Hashes for koerce-0.1.1-cp312-cp312-win_amd64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8e6d1a064ea06e51cee0bae91577dd0ca927376f804161c6cba7a2014bef2e4b |
|
MD5 | e3145034d1fccead9e5a1ff5568df873 |
|
BLAKE2b-256 | cae5b75e921fbb08b927146b67e7512240edf1d9aa42c587d30178ffc0e15dff |
Hashes for koerce-0.1.1-cp312-cp312-win32.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4e26a537502527f062c0110ffe99cdbfa1f71c24ec35eda8c45f94c56c10c0bf |
|
MD5 | 4c9f49a4a1139828488f6fbe9c12a578 |
|
BLAKE2b-256 | 44c2517a99bae4fdb24ebcc4434fd3b6579d3c4984490f0a56b95cfd97f2c8b5 |
Hashes for koerce-0.1.1-cp312-cp312-musllinux_1_2_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 657c55e4452a6ba65d41f3d709b168d4b75a91b07ccd8a6ef97afcc169fa2640 |
|
MD5 | 005b962b09fe9572a5fe432ad0679f17 |
|
BLAKE2b-256 | af6dd412172294c0a054787e5463a1d23d3f0bde969227e626e17f8527bc6bb7 |
Hashes for koerce-0.1.1-cp312-cp312-musllinux_1_2_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9f4a72d342d65172b0e3b32062f6aee49582bfb701aafa52f770bc2fa20e45a1 |
|
MD5 | d356f367196f771b4d7b4f2776203e73 |
|
BLAKE2b-256 | 38bd89b24ff9323bd33945a571339df6dad00b8c767c951e7cec7158e4e1f585 |
Hashes for koerce-0.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1136b4aa4e97cc7170249f5d6057562866137c8edc52e27c49e10b2cb394f73c |
|
MD5 | 4db7ffa8e4bdf28f6bdb306ac61ac6a2 |
|
BLAKE2b-256 | 2ce4027a59b0cd16d4c48afb6cef393de8230e4d05a7b9e1225c365518f5d4b6 |
Hashes for koerce-0.1.1-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | e3ed54d8eb3b4f10f57b99cec3034f267e0fcca6954da30f1c08d2c5a4fd5960 |
|
MD5 | d9e92b01d98f2a601f9e805cb9333ea8 |
|
BLAKE2b-256 | b2ec9cabbb2be7f86c04091f0f6ab7fd02a4de59962a81416f5aaf7960d41099 |
Hashes for koerce-0.1.1-cp312-cp312-macosx_14_0_arm64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | c8849eff359a67a6fea846945ac8a810c5fdbb105b7b523a5c0f4c9c1bba9a1f |
|
MD5 | 97ed6f31beda7c09e7aaee5ea9d4248a |
|
BLAKE2b-256 | 7bc6a2c72bd50b85e49dce885e2a2f8ffb4a604d1be61109bc18e3bc0d61a21f |
Hashes for koerce-0.1.1-cp312-cp312-macosx_13_0_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | dd4319f73e8db5af94eab69bb90012f0c5d61405fff0f141d6637fa32c551c93 |
|
MD5 | 18389005764fa597f0ed97388a056ae0 |
|
BLAKE2b-256 | 26c3fc6c01e648867e94cabcbf5e0960bdeacda5e3e60e370b1a51f083d869d5 |
Hashes for koerce-0.1.1-cp311-cp311-win_amd64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9b21486d909935bd1692c01d4256df0d66bccc27cb32346fb6780e2537f29b1c |
|
MD5 | 0f468c7679673d1567278408473051c0 |
|
BLAKE2b-256 | 9814f321d1e1989094025016cf4d8bbf6716691e9f8cd8d79c4af95756f485d6 |
Hashes for koerce-0.1.1-cp311-cp311-win32.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 253a87dafcca912fbf13c2c77a2e8cc8646d9a58f5b44b14b820ae141f390206 |
|
MD5 | 33dc61b3a76c4c0eb512d6009fc0e6b8 |
|
BLAKE2b-256 | ed7f6cafaeb1af698d5a64319b70fe47263eb4b521a3cb95f698ae9dac3edfa7 |
Hashes for koerce-0.1.1-cp311-cp311-musllinux_1_2_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 487b5a56604506af1f128cb8bc72209113630895395948ed2c0682a3a4737ef0 |
|
MD5 | 5504e8624a15dca8e1af255ef54ec8e2 |
|
BLAKE2b-256 | 8ecc04ce713931f5547155d75b45f57e05e3676fce5fd5dfffc5e0be8cb56d0d |
Hashes for koerce-0.1.1-cp311-cp311-musllinux_1_2_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | fee7d6b2e2af9e7ae5d2bdf873f701e161017160acf1389f9183b8e014db1caa |
|
MD5 | fdebddcb4c74084e4e87480825c40158 |
|
BLAKE2b-256 | f3480d8001db100a78df10c7b1b8ce97dd551baaa90175d2aa5f208e587ac4b0 |
Hashes for koerce-0.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | deb0a8bfecd96c3b1e8bd55f170e7c27e85fa600fea08ebfab5f548f9566258d |
|
MD5 | a26159e2272ab59b1795e75bed86075c |
|
BLAKE2b-256 | 947b200d534648281bed24db736518aa97c088b8ce4ee1768204f33a3f243092 |
Hashes for koerce-0.1.1-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2e790153458926ba3df40d603ad5ae74d1a29ac44baac1202143125d8098694a |
|
MD5 | bfb01161f5b51113d20f72405c2c470d |
|
BLAKE2b-256 | 841412367e176dd364c725a32fb5a7f55ebd9b79f364a240f9cc2d5de3127157 |
Hashes for koerce-0.1.1-cp311-cp311-macosx_14_0_arm64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ae8794e4e28130bff745b4fb58769f43946023ab58f19f2f1f1d29a122b638d6 |
|
MD5 | 4886c4526072556aea5802f7f780bb1d |
|
BLAKE2b-256 | dc55cd62b9b8485bb91d740c3662b809c6f65fea79789c203970e226bf88df72 |
Hashes for koerce-0.1.1-cp311-cp311-macosx_13_0_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 659dd63210771f53141bd0383da5d878260987d697ee29e63c641639ac87aca9 |
|
MD5 | 2ec3ac388e9d95514b73a382cc40c7d6 |
|
BLAKE2b-256 | de097f2de14498c8d0deae8c3ab3e5bc1321a741ad1d585a353f04a124e85314 |
Hashes for koerce-0.1.1-cp310-cp310-win_amd64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0c24db21713ba865cb1f484f11b3f3e6848d72ddc7ecc65ca4e24fa610d7fcdb |
|
MD5 | eedb5282d2cda7bbacd656cd2526f38f |
|
BLAKE2b-256 | 4a191d68c4475fecaee1dfd95d1c11ef6eb9904554715a68b657dc1c2b8e4d3f |
Hashes for koerce-0.1.1-cp310-cp310-win32.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | c1124843cdc8ee7dfe44d8f89d98327b571db0a1b8e58105596d8394452cd3ac |
|
MD5 | ab0cded3356f4d645369e8f7a8a433da |
|
BLAKE2b-256 | 988dba6b0f0183e9b03919219a2a7457078700006dd3513f170a99ce1208002a |
Hashes for koerce-0.1.1-cp310-cp310-musllinux_1_2_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 7e2562b672617611513c8a2536ac1f0b8d98f9cdf246ea7c8513ed50a16a58aa |
|
MD5 | 56cc0f06fe871adda781790662ce146a |
|
BLAKE2b-256 | c4b4a0dba997dfcce4c84244c55ebedfb2609c0df96c4d677399b80d0fe4b111 |
Hashes for koerce-0.1.1-cp310-cp310-musllinux_1_2_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2ddafb52b8e119c25725d45900bb4f9d5b4eae754f8b6f85eff38aa0ba14b91d |
|
MD5 | 9b3d929199d89963e59515384f90d820 |
|
BLAKE2b-256 | d065e2c8ef05aa187ebeb180d6f39dc40faee2fa1573ba65426e280646fdffe4 |
Hashes for koerce-0.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 79dba50a52087636145ab1496f93c3f4eeb751c0ca07b3b9f73a0701f0e6948a |
|
MD5 | a01a1b819413495bf6cb681ec7981d3a |
|
BLAKE2b-256 | 18de252a342b4964e75ed46d4a190c67b417908611f4bdfb30636ffc565355a5 |
Hashes for koerce-0.1.1-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 199363731654cc4063f2076d607e8b2fcae882b8d2c2f30befc62c346687b8d4 |
|
MD5 | 562e68293248d016fd2d5f0c401cb651 |
|
BLAKE2b-256 | 8a173365f524f35a2ab0eb8328aef2794f2f5941d2610e356df31936e687e118 |
Hashes for koerce-0.1.1-cp310-cp310-macosx_14_0_arm64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 48ddd4b4d0a2b44393b085ab6c15ab820743542efff867f8da60beac251f627d |
|
MD5 | 603bab3aace549aa156f235ca0d06378 |
|
BLAKE2b-256 | 10e103c147e70e96f43e1fab2d142f2fe184cba7d00262e500e5a089b17ae903 |
Hashes for koerce-0.1.1-cp310-cp310-macosx_13_0_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 536b579622d67849faa509a987887c59a8901e477dfc3a3508f588b7f9e56aa1 |
|
MD5 | a1e2adb1d1dd92a27168e7fbd129f676 |
|
BLAKE2b-256 | 73169cd85a31282d6e93b54790a99e8aaa7bb62ee569bdc1b7b11d77da535f64 |