# cmplib
cmplib is a library used for writing composable matchers for your tests,
validators etc. cmplib's matchers are compared for equality (or inequality)
against your values.
Each matcher provides a human-readable string representation (implemented
by `__repr__` dunder method).
## List of Matchers
### Eq, Ne, Gt, Ge, Lt, Le, Not
Check whether a checked-against value is equal to value stored in matcher,
not equal, greater than, greater or equal, less than or less or equal. Using
of `Not` negates the meaning of stored sub-matcher or value.
```python
assert "abc" == Eq("abc")
assert "abc" == Eq(And(Contains("b"), Len(3)))
assert 1 != Eq(2)
assert 1 == Gt(0)
assert False == Not(Truish())
assert True != Not(Truish())
assert 1 == Not(Gt(1))
assert 1 == Not(2)
```
### Is
Check whether a value is the same object as the one stored in a matcher.
```python
obj = object()
assert obj == Is(obj)
assert list() != Is(list())
```
### IsNone
Check whether a value is `None`.
```python
assert None == IsNone()
assert 0 == Not(IsNone())
assert [] != IsNone()
```
### And, Or
Check whether all sub-matchers match the value (for `All`), or any of them
(for `Or`).
```python
l = [1, 2, 3]
assert l == And(Contains(1), Contains(2))
assert l != And(Contains(1), Contains(2), Contains(3), Contains(4))
assert l == Or(Contains(5), Contains(2))
assert l != Or(Contains(5), Contains(6), Contains(7), Contains(8))
```
Instead of passing matchers directly to `And` or `Or`, they can be composed
with bitwise and operator (`&`) and bitwise or operator (`|`).
```python
l = [1, 2, 3]
assert l == Contains(1) & Contains(2)
assert l == Contains(5) | Contains(2)
```
### Truish
Check whether value casts to `True` boolean, as in `bool(value)`.
```python
assert True == Truish()
assert 1 == Truish()
assert "foo" == Truish()
assert False != Truish()
```
### Len
Check whether container's size matches the matcher.
```python
assert [] == Len(0)
assert [1, 2, 3, "aaa"] == Len(And(Gt(0), Lt(5)))
assert [1, 2, 3, "aaa"] != Len(And(Gt(0), Lt(4)))
```
### IsEmpty
Check whetner container is empty (it's size is 0).
```python
assert [] == IsEmpty()
assert "" == IsEmpty()
assert dict() == IsEmpty()
assert [1] != IsEmpty()
assert "a" != IsEmpty()
```
### Each
Check whether each element of iterable matches the stored sub-matcher or
value.
```python
assert [1, 1, 1] == Each(1)
assert [1, 2, 3] == Each(Not(0))
```
### Values
Check whether iterable contains at least 1 value which matches a stored value
or sub-matcher. Number of required matching values may be changed with
keyword-only argument `at_least`.
_Implementation detail_: for checking whether a container contains a single
value, it's better to use `Contains` which will perform better for
certain types of containers (like sets or dictionaries).
```python
assert [0, 2, 3, 1, 2] == Values(1)
assert [] == Values(1, at_least=0)
assert [0, 2, 3, 1, 2] == Values(Gt(1), at_least=2)
assert [] != Values(1)
assert [0, 2, 3, 1, 2] != Values(1, at_least=2)
```
### Contains
Check whether iterable contains a value which matches stored value or
sub-matcher.
```python
assert [1, 2, 3] == Contains(1)
assert [1, 2, 3] == Contains(Or("a", 2, "b"))
```
### Unordered
Check whether iterable matches stored values or matchers. Each item must
match exactly one matcher, but matching order doesn't matter.
```python
assert [] == Unordered()
assert [1, 2, 3] == Unordered(3, 2, 1)
assert [1, 2, 3] == Unordered(Eq(3), Or(7, 2), 1)
assert [1, 2, 3] != Unordered("1", "2", "3")
```
To perform ordered search, simply construct an iterable of matchers.
```python
assert [1, 2, 3] == [Eq(1), Or(7, 2), 3]
assert [1, 2, 3] != [Eq(3), Or(7, 2), 1]
```
### KeyEq, AttrEq
For `KeyEq` check whether a object stores a value under a key. Key can be
either a key in a dict-like object, or index in a list-like or tuple-like
objects.
```python
d = {"foo": "bar", "baz": "blah"}
assert d == KeyEq("foo", "bar")
assert d == KeyEq("foo", Not(IsEmpty()))
assert d == KeyEq("foo", Not(Contains("h")) & Contains("b") & Contains("a"))
lst = ["foo", "bar"]
assert lst == KeyEq(1, "bar")
assert lst == KeyEq(-1, "bar")
```
For `AttrEq` check whether an object stores a value under attribute.
```python
@dataclass
class Foo:
foo: str = "bar"
baz: str = "blah"
o = Foo()
assert o == AttrEq("foo", "bar")
assert o != AttrEq("foo", "blah")
```
### IsInstance
Check whether a value is an instance of a given type.
```python
assert 1 == IsInstance(int)
assert datetime.now() == IsInstance(datetime)
```
### Object, DictFields
Composes a matcher from a keyword-only arguments. Each of these arguments
must match a corresponding attribute (for `Object`) or key (for `DictFields`)
of checked item.
`Object` additionally accepts a single optional, non-keyword argument, which
matches against a type of matched item.
These are convenience matchers. The same effect can be accomplished by using
bare `AttrEq` and `KeyEq` matchers.
```python
@dataclass
class Foo:
foo: int = 1
bar: int = 2
baz: str = "s"
o: Optional["Foo"] = None
assert Foo() == Object(foo=1, bar=Ge(2))
assert Foo(o=Foo()) == Object(foo=1, o=Object(bar=2, o=None))
assert Foo() != Object(dict, foo=1)
assert Foo() != Object(nonexisting=1)
d = {"foo": 1, "bar": 2, "baz": "s", "o": {"foo": 1}}
assert d == DictFields(foo=1, bar=Ge(2))
assert d == DictFields(foo=1, o=DictFields(foo=1))
assert d != DictFields(foo=1, o=DictFields(foo=2))
assert d != DictFields(foo=1, o=DictFields(nonexisting=2))
```
### Items
Composes a matcher from a index-matcher pairs. Each matcher must match a
value in the compared container under the corresponding index.
```python
lst = ["foo", "bar", "baz", "blah"]
assert lst == Items((0, "foo"), (-1, "blah"))
assert lst == Items((-1, "blah"), (0, "foo"))
assert lst != Items((0, "foo"), (-1, "blah2"))
assert lst != Items((0, "foo"), (100, "foo"))
```
### Glob
Test whether a astring matches a given globbing (wildcard) expression.
Matching rules of `fnmatch.fnmatchcase` apply.
```python
assert "foo" == Glob("*")
assert ["foo", "bar"] == [Glob("f*"), Glob("b*")]
```
`Glob` module supports the following wildcards (same as `fnmatch` module):
- `*`: matches everything
- `?`: matches any single character
- `[seq]`: matches any character in seq
- `[!seq]`: matches any character not in seq
It is possible to automatically convert checked values to string. This way
non-string types which support such conversion can be matched as well.
```python
from pathlib import Path
assert Path("/foo/bar") == Glob("/foo/*", coerce=True)
```
Matching is case-sensitive, but it can be changed to case-insensitive by
passing `case=False`.
```python
assert "Foo" == Glob("f*", case=False)
assert "Foo" != Glob("f*")
```
### Re
Test whether a astring matches a given regular expression. Matching rules of
`re.match` apply. `Re` matcher doesn't store capture groups.
```python
assert "foo bar baz" == Re("^foo.*z$")
assert ["foo", "bar"] == [Re("f.*"), Re("b.*")]
```
It is possible to automatically convert checked values to string. This way
non-string types which support such conversion can be matched as well.
```python
assert 11 == Re(r"\d+", coerce=True)
```
`Re` matcher accepts the same flags as functions in ordinary `re` module.
```python
import re
assert "fOO bar baz\n" == Re("Foo.*", flags = re.IGNORECASE | re.DOTALL)
```
### CanBeTimestamp
Test whether a value can be converted to a UNIX timestamp. UNIX timestamps
are floating point numbers, so this means that any value which can be
converted to the correct float are considered as such.
```python
assert 0 == CanBeTimestamp()
assert "0" == CanBeTimestamp()
assert "0.123" == CanBeTimestamp()
assert datetime.now().timestamp() == CanBeTimestamp()
assert "" != CanBeTimestamp()
assert datetime.now() != CanBeTimestamp()
```
### IsIsoDateTime
Check whether a value represents a valid datetime (either a `date` or
`datetime` object or is a value which follows ISO 8601 format and can be
converted to such value).
```python
assert datetime.now() == IsIsoDateTime()
assert datetime.today() == IsIsoDateTime()
assert datetime.now().isoformat() == IsIsoDateTime()
assert datetime.today().isoformat() == IsIsoDateTime()
assert "2021-01-01" == IsIsoDateTime()
assert "2022-03" != IsIsoDateTime()
```
### IsUnique
Caches values which were already checked against `IsUnique` and matches them
as long as no such value was previously matched. It is possible to specify a
cache name which should hold values. Values are only compared against other
values stored in the same cache. It is possible to clear a particular cache
or all caches
```python
assert 1 == IsUnique("cache-1")
assert 1 == IsUnique("cache-2")
assert 2 == IsUnique("cache-1")
assert 1 != IsUnique("cache-1")
IsUnique.clear("cache-1")
assert 1 == IsUnique("cache-1")
assert 1 != IsUnique("cache-2")
IsUnique.clear()
assert 1 == IsUnique("cache-1")
assert 1 == IsUnique("cache-2")
```
Creating `IsUnique` without a cache name results in generating a new cache
for each instance created this way.
```python
U = IsUnique()
assert 1 == U
assert 1 != U
assert 1 == IsUnique()
assert 1 == IsUnique()
assert [1, 2, 3, 4] == Each(IsUnique())
assert [1, 2, 3, 4, 1, 1, 7] != Each(IsUnique())
```
### Fn
Matches true when a function called with a value passed as the first argument
returns True. When `coerce` is set to True, stored function doesn't have to
return a boolean, but instead its return value is casted to boolean.
```python
assert 1 == Fn(lambda x: x == 1)
assert "1" == Fn((lambda x: x), coerce=True)
```
### SKIP
`SKIP` is a sentinel which always matches true and can be used to mark not
interesing items.
```python
assert [1, 2, 3] == [SKIP, SKIP, SKIP]
assert [1, 2, 3] == [1, SKIP, SKIP]
```
## License
cmplib is licensed under the terms of LGPLv3.
Raw data
{
"_id": null,
"home_page": "https://git.goral.net.pl/cmplib.git/about",
"name": "cmplib",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.8",
"maintainer_email": null,
"keywords": null,
"author": "Michal Goral",
"author_email": "dev@goral.net.pl",
"download_url": "https://files.pythonhosted.org/packages/99/11/50fb26a3f8f32270ffe60cebfd32d1104e0cfa290279bf11599861094acb/cmplib-1.3.0.tar.gz",
"platform": null,
"description": "# cmplib\n\ncmplib is a library used for writing composable matchers for your tests,\nvalidators etc. cmplib's matchers are compared for equality (or inequality)\nagainst your values.\n\nEach matcher provides a human-readable string representation (implemented\nby `__repr__` dunder method).\n\n## List of Matchers\n\n### Eq, Ne, Gt, Ge, Lt, Le, Not\n\nCheck whether a checked-against value is equal to value stored in matcher,\nnot equal, greater than, greater or equal, less than or less or equal. Using\nof `Not` negates the meaning of stored sub-matcher or value.\n\n```python\nassert \"abc\" == Eq(\"abc\")\nassert \"abc\" == Eq(And(Contains(\"b\"), Len(3)))\nassert 1 != Eq(2)\nassert 1 == Gt(0)\n\nassert False == Not(Truish())\nassert True != Not(Truish())\nassert 1 == Not(Gt(1))\nassert 1 == Not(2)\n```\n\n### Is\n\nCheck whether a value is the same object as the one stored in a matcher.\n\n```python\nobj = object()\n\nassert obj == Is(obj)\nassert list() != Is(list())\n```\n\n### IsNone\n\nCheck whether a value is `None`.\n\n```python\nassert None == IsNone()\nassert 0 == Not(IsNone())\nassert [] != IsNone()\n```\n\n### And, Or\n\nCheck whether all sub-matchers match the value (for `All`), or any of them\n(for `Or`).\n\n```python\nl = [1, 2, 3]\nassert l == And(Contains(1), Contains(2))\nassert l != And(Contains(1), Contains(2), Contains(3), Contains(4))\nassert l == Or(Contains(5), Contains(2))\nassert l != Or(Contains(5), Contains(6), Contains(7), Contains(8))\n```\n\nInstead of passing matchers directly to `And` or `Or`, they can be composed\nwith bitwise and operator (`&`) and bitwise or operator (`|`).\n\n```python\nl = [1, 2, 3]\nassert l == Contains(1) & Contains(2)\nassert l == Contains(5) | Contains(2)\n```\n\n### Truish\n\nCheck whether value casts to `True` boolean, as in `bool(value)`.\n\n```python\nassert True == Truish()\nassert 1 == Truish()\nassert \"foo\" == Truish()\nassert False != Truish()\n```\n\n### Len\n\nCheck whether container's size matches the matcher.\n\n```python\nassert [] == Len(0)\nassert [1, 2, 3, \"aaa\"] == Len(And(Gt(0), Lt(5)))\nassert [1, 2, 3, \"aaa\"] != Len(And(Gt(0), Lt(4)))\n```\n\n### IsEmpty\n\nCheck whetner container is empty (it's size is 0).\n\n```python\nassert [] == IsEmpty()\nassert \"\" == IsEmpty()\nassert dict() == IsEmpty()\n\nassert [1] != IsEmpty()\nassert \"a\" != IsEmpty()\n```\n\n### Each\n\nCheck whether each element of iterable matches the stored sub-matcher or\nvalue.\n\n```python\nassert [1, 1, 1] == Each(1)\nassert [1, 2, 3] == Each(Not(0))\n```\n\n### Values\n\nCheck whether iterable contains at least 1 value which matches a stored value\nor sub-matcher. Number of required matching values may be changed with\nkeyword-only argument `at_least`.\n\n_Implementation detail_: for checking whether a container contains a single\nvalue, it's better to use `Contains` which will perform better for\ncertain types of containers (like sets or dictionaries).\n\n```python\nassert [0, 2, 3, 1, 2] == Values(1)\nassert [] == Values(1, at_least=0)\nassert [0, 2, 3, 1, 2] == Values(Gt(1), at_least=2)\n\nassert [] != Values(1)\nassert [0, 2, 3, 1, 2] != Values(1, at_least=2)\n```\n\n### Contains\n\nCheck whether iterable contains a value which matches stored value or\nsub-matcher.\n\n```python\nassert [1, 2, 3] == Contains(1)\nassert [1, 2, 3] == Contains(Or(\"a\", 2, \"b\"))\n```\n\n### Unordered\n\nCheck whether iterable matches stored values or matchers. Each item must\nmatch exactly one matcher, but matching order doesn't matter.\n\n```python\nassert [] == Unordered()\nassert [1, 2, 3] == Unordered(3, 2, 1)\nassert [1, 2, 3] == Unordered(Eq(3), Or(7, 2), 1)\n\nassert [1, 2, 3] != Unordered(\"1\", \"2\", \"3\")\n```\n\nTo perform ordered search, simply construct an iterable of matchers.\n\n```python\nassert [1, 2, 3] == [Eq(1), Or(7, 2), 3]\nassert [1, 2, 3] != [Eq(3), Or(7, 2), 1]\n```\n\n### KeyEq, AttrEq\n\nFor `KeyEq` check whether a object stores a value under a key. Key can be\neither a key in a dict-like object, or index in a list-like or tuple-like\nobjects.\n\n```python\nd = {\"foo\": \"bar\", \"baz\": \"blah\"}\nassert d == KeyEq(\"foo\", \"bar\")\nassert d == KeyEq(\"foo\", Not(IsEmpty()))\nassert d == KeyEq(\"foo\", Not(Contains(\"h\")) & Contains(\"b\") & Contains(\"a\"))\n\n\nlst = [\"foo\", \"bar\"]\nassert lst == KeyEq(1, \"bar\")\nassert lst == KeyEq(-1, \"bar\")\n```\n\nFor `AttrEq` check whether an object stores a value under attribute.\n\n```python\n@dataclass\nclass Foo:\n foo: str = \"bar\"\n baz: str = \"blah\"\n\no = Foo()\n\nassert o == AttrEq(\"foo\", \"bar\")\nassert o != AttrEq(\"foo\", \"blah\")\n```\n\n### IsInstance\n\nCheck whether a value is an instance of a given type.\n\n```python\nassert 1 == IsInstance(int)\nassert datetime.now() == IsInstance(datetime)\n```\n\n### Object, DictFields\n\nComposes a matcher from a keyword-only arguments. Each of these arguments\nmust match a corresponding attribute (for `Object`) or key (for `DictFields`)\nof checked item.\n\n`Object` additionally accepts a single optional, non-keyword argument, which\nmatches against a type of matched item.\n\nThese are convenience matchers. The same effect can be accomplished by using\nbare `AttrEq` and `KeyEq` matchers.\n\n```python\n@dataclass\nclass Foo:\n foo: int = 1\n bar: int = 2\n baz: str = \"s\"\n o: Optional[\"Foo\"] = None\n\nassert Foo() == Object(foo=1, bar=Ge(2))\nassert Foo(o=Foo()) == Object(foo=1, o=Object(bar=2, o=None))\n\nassert Foo() != Object(dict, foo=1)\nassert Foo() != Object(nonexisting=1)\n\nd = {\"foo\": 1, \"bar\": 2, \"baz\": \"s\", \"o\": {\"foo\": 1}}\nassert d == DictFields(foo=1, bar=Ge(2))\nassert d == DictFields(foo=1, o=DictFields(foo=1))\n\nassert d != DictFields(foo=1, o=DictFields(foo=2))\nassert d != DictFields(foo=1, o=DictFields(nonexisting=2))\n```\n\n### Items\n\nComposes a matcher from a index-matcher pairs. Each matcher must match a\nvalue in the compared container under the corresponding index.\n\n```python\nlst = [\"foo\", \"bar\", \"baz\", \"blah\"]\nassert lst == Items((0, \"foo\"), (-1, \"blah\"))\nassert lst == Items((-1, \"blah\"), (0, \"foo\"))\n\nassert lst != Items((0, \"foo\"), (-1, \"blah2\"))\nassert lst != Items((0, \"foo\"), (100, \"foo\"))\n```\n\n### Glob\n\nTest whether a astring matches a given globbing (wildcard) expression.\nMatching rules of `fnmatch.fnmatchcase` apply.\n\n```python\nassert \"foo\" == Glob(\"*\")\nassert [\"foo\", \"bar\"] == [Glob(\"f*\"), Glob(\"b*\")]\n```\n\n`Glob` module supports the following wildcards (same as `fnmatch` module):\n\n- `*`: matches everything\n- `?`: matches any single character\n- `[seq]`: matches any character in seq\n- `[!seq]`: matches any character not in seq\n\nIt is possible to automatically convert checked values to string. This way\nnon-string types which support such conversion can be matched as well.\n\n```python\nfrom pathlib import Path\nassert Path(\"/foo/bar\") == Glob(\"/foo/*\", coerce=True)\n```\n\nMatching is case-sensitive, but it can be changed to case-insensitive by\npassing `case=False`.\n\n```python\nassert \"Foo\" == Glob(\"f*\", case=False)\nassert \"Foo\" != Glob(\"f*\")\n```\n\n### Re\n\nTest whether a astring matches a given regular expression. Matching rules of\n`re.match` apply. `Re` matcher doesn't store capture groups.\n\n```python\nassert \"foo bar baz\" == Re(\"^foo.*z$\")\nassert [\"foo\", \"bar\"] == [Re(\"f.*\"), Re(\"b.*\")]\n```\n\nIt is possible to automatically convert checked values to string. This way\nnon-string types which support such conversion can be matched as well.\n\n```python\nassert 11 == Re(r\"\\d+\", coerce=True)\n```\n\n`Re` matcher accepts the same flags as functions in ordinary `re` module.\n\n```python\nimport re\nassert \"fOO bar baz\\n\" == Re(\"Foo.*\", flags = re.IGNORECASE | re.DOTALL)\n```\n\n### CanBeTimestamp\n\nTest whether a value can be converted to a UNIX timestamp. UNIX timestamps\nare floating point numbers, so this means that any value which can be\nconverted to the correct float are considered as such.\n\n```python\nassert 0 == CanBeTimestamp()\nassert \"0\" == CanBeTimestamp()\nassert \"0.123\" == CanBeTimestamp()\nassert datetime.now().timestamp() == CanBeTimestamp()\n\nassert \"\" != CanBeTimestamp()\nassert datetime.now() != CanBeTimestamp()\n```\n\n### IsIsoDateTime\n\nCheck whether a value represents a valid datetime (either a `date` or\n`datetime` object or is a value which follows ISO 8601 format and can be\nconverted to such value).\n\n```python\nassert datetime.now() == IsIsoDateTime()\nassert datetime.today() == IsIsoDateTime()\nassert datetime.now().isoformat() == IsIsoDateTime()\nassert datetime.today().isoformat() == IsIsoDateTime()\nassert \"2021-01-01\" == IsIsoDateTime()\n\nassert \"2022-03\" != IsIsoDateTime()\n```\n\n### IsUnique\n\nCaches values which were already checked against `IsUnique` and matches them\nas long as no such value was previously matched. It is possible to specify a\ncache name which should hold values. Values are only compared against other\nvalues stored in the same cache. It is possible to clear a particular cache\nor all caches\n\n```python\nassert 1 == IsUnique(\"cache-1\")\nassert 1 == IsUnique(\"cache-2\")\nassert 2 == IsUnique(\"cache-1\")\n\nassert 1 != IsUnique(\"cache-1\")\n\nIsUnique.clear(\"cache-1\")\nassert 1 == IsUnique(\"cache-1\")\nassert 1 != IsUnique(\"cache-2\")\n\nIsUnique.clear()\nassert 1 == IsUnique(\"cache-1\")\nassert 1 == IsUnique(\"cache-2\")\n```\n\nCreating `IsUnique` without a cache name results in generating a new cache\nfor each instance created this way.\n\n```python\nU = IsUnique()\nassert 1 == U\nassert 1 != U\n\nassert 1 == IsUnique()\nassert 1 == IsUnique()\nassert [1, 2, 3, 4] == Each(IsUnique())\nassert [1, 2, 3, 4, 1, 1, 7] != Each(IsUnique())\n```\n\n### Fn\n\nMatches true when a function called with a value passed as the first argument\nreturns True. When `coerce` is set to True, stored function doesn't have to\nreturn a boolean, but instead its return value is casted to boolean.\n\n```python\nassert 1 == Fn(lambda x: x == 1)\nassert \"1\" == Fn((lambda x: x), coerce=True)\n```\n\n### SKIP\n\n`SKIP` is a sentinel which always matches true and can be used to mark not\ninteresing items.\n\n```python\nassert [1, 2, 3] == [SKIP, SKIP, SKIP]\nassert [1, 2, 3] == [1, SKIP, SKIP]\n```\n\n## License\n\ncmplib is licensed under the terms of LGPLv3.\n",
"bugtrack_url": null,
"license": "LGPL-3.0-only",
"summary": "Library for writing composable predicates",
"version": "1.3.0",
"project_urls": {
"Documentation": "https://git.goral.net.pl/cmplib.git/about",
"Homepage": "https://git.goral.net.pl/cmplib.git/about",
"Repository": "https://git.goral.net.pl/cmplib.git"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "1207182eeb7fdb7fc3a08fc6948a97d5ef0d6d68e6f1da8ecd3b21c0b579c1c6",
"md5": "b92efc0ca59b866ad7c3b5b9166a577d",
"sha256": "a081b9d5ae96d6bccd5503103f1ded4d07003d077243a744f0c7f2a61c5f9baa"
},
"downloads": -1,
"filename": "cmplib-1.3.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "b92efc0ca59b866ad7c3b5b9166a577d",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.8",
"size": 21151,
"upload_time": "2024-07-01T13:47:20",
"upload_time_iso_8601": "2024-07-01T13:47:20.811167Z",
"url": "https://files.pythonhosted.org/packages/12/07/182eeb7fdb7fc3a08fc6948a97d5ef0d6d68e6f1da8ecd3b21c0b579c1c6/cmplib-1.3.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "991150fb26a3f8f32270ffe60cebfd32d1104e0cfa290279bf11599861094acb",
"md5": "bb6f47cc4e12a2b24a0b06936d6179ad",
"sha256": "f90d89c351a7f2b8192a107287e3b36e9f4e4b7a49af4b1ce752e9b6234bf48e"
},
"downloads": -1,
"filename": "cmplib-1.3.0.tar.gz",
"has_sig": false,
"md5_digest": "bb6f47cc4e12a2b24a0b06936d6179ad",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.8",
"size": 19732,
"upload_time": "2024-07-01T13:47:22",
"upload_time_iso_8601": "2024-07-01T13:47:22.345278Z",
"url": "https://files.pythonhosted.org/packages/99/11/50fb26a3f8f32270ffe60cebfd32d1104e0cfa290279bf11599861094acb/cmplib-1.3.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-07-01 13:47:22",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "cmplib"
}