affinitree


Nameaffinitree JSON
Version 0.21.1 PyPI version JSON
download
home_pageNone
SummaryDistillation of piece-wise linear neural networks into decision trees
upload_time2024-03-08 17:05:11
maintainerNone
docs_urlNone
authorNone
requires_python>=3.7
licenseApache 2.0
keywords affinitree afftree distillation xai
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Welcome to Affinitree!

[![PyPi](https://img.shields.io/pypi/v/affinitree)](https://pypi.org/project/affinitree/)

Affinitree is an open-source library to generate faithful surrogate models (decision trees) from pre-trained piece-wise linear neural networks.
The core part of the library is implemented in Rust for best performance.

The opaque nature of neural networks stands in the way of their widespread usage, especially in
safety critical domains.
To tackle such tasks, it is important to access the semantics of neural networks in a structured way.
One promising field of XAI research is concerned with finding alternative models for a given neural network
that are more transparent, interpretable, or easier to analyze. 
This approach is called *model distillation* and the resulting model is called *surrogate model*, *proxy model* or *white-box model*.

`Affinitree` is a library that allows to distil a rectifier neural network into a specialized decision tree.
In XAI literature, it is generally accepted that decision trees are one of a few model classes that are
comprehensible by humans.
This makes decision trees a prime candidate for surrogate models.
Further, decision trees lend themselves well for the representation of neural networks with ReLU activation.
Due to the ReLU activation function the networks are piece-wise linear, a fact that `affinitree` uses to distill the network into a decision tree.

Commonly, surrogate models are only an approximation of the true nature of the neural network.
In contrast, `affinitree` provides mathematical sound and correct surrogate models.
This is achieved by an holistic symbolic execution of the network.
The resulting decision structure is human understandable but size must be controlled.

# Installation

**affinitree** requires Python 3.8 - 3.12.

```sh
pip install affinitree
```

Wheels are currently available for linux (x86_64).
For other architectures, see [Build Wheels from Rust](#build-wheels-from-rust).

# First Steps

`Affinitree` provides a high-level API to convert a pre-trained ``pytorch`` model into a decision tree (requires installation of `torch`). 

```python 
from torch import nn
from affinitree.pytorch import from_pytorch

model = nn.Sequential(nn.Linear(7, 5),
                      nn.ReLU(),
                      nn.Linear(5, 5),
                      nn.ReLU(),
                      nn.Linear(5, 4)
                     )

dd = from_pytorch(model)
```

It may be noted that `affinitree` is independent of any concrete neural network library.
The function `from_pytorch` is just a thin wrapper that extracts numpy arrays from pytorch models for convenience.
Any model expressed as a sequence of numpy matrices can be read (see also the provided [examples](examples)).

After distilling the model, one can use the resulting `AffTree` for plotting the decision tree
in [graphviz's](https://graphviz.org/) DOT language:

```python
dot_str(dd)
```

A simple AffTree may look like this (representing the xor function):

<p align="center">
  <img alt="fig:afftree example (at github)" height="300" src="figures/afftree_example.svg"/>
</p>

`Affinitree` provides a method to plot the decision boundaries of an ``AffTree`` using `matplotlib`

```python
from affinitree.plot import plot_preimage_partition, LedgerDiscrete

# derive 10 colors from the tab10 color map and position ledgend at the top 
ledger = LedgerDiscrete(cmap='tab10', num=10, position='top')
# map the terminals of dd to one of the 10 colors based on their class
ledger.fit(dd)
# plot for each terminal of dd the polytope that is implied by the path from the root to the respective terminal
plot_preimage_partition(dd, ledger, intervals=[(-20., 15.), (-12., 12.)])
```
The ``ledger`` is used to control the coloring and legend of the plot.
A resulting plot may look like this:

<p align="center">
  <img alt="fig:mnist preimage partition (at github)" height="400" src="figures/mnist_preimage_partition.svg"/>
</p>

## Composition and Schemas

Many operations on ``AffTree``s can be implemented using composition.
For example, here are a few common functions in the realm of neural networks expressed as ``AffTree``.

ReLU (indim=4):

<p align="center">
    <img alt="fig:relu" height="400" src="figures/relu_4.svg"/>
</p>

ReLU (indim=4) applied only to the first component (partial ReLU / step ReLU):

<p align="center">
    <img alt="fig:partial-relu" height="150" src="figures/partial_relu_4_0.svg"/>
</p>

Argmax (indim=4):

<p align="center">
    <img alt="fig:argmax" height="300" src="figures/argmax_4.svg"/>
</p>

Schemas are a collection of typical operations that are used in the context of neural networks.
In code, on can apply these as follows:

```python
from affinitree import AffTree
from affinitree import schema

dd = AffTree.identity(2)

relu = schema.ReLU(2)
dd = dd.compose(relu)
```

The following operations are already provided:

```python
schema.ReLU(dim=n)
schema.partial_ReLU(dim=n, row=m)
schema.argmax(dim=n)
schema.inf_norm(minimum=a, maximum=b)
schema.class_characterization(dim=n, clazz=c)
```

The interface is easily adaptable to additional needs, one just needs to define a function that returns an ``AffTree`` instance that expresses the required piece-wise linear function.

# Build Wheels from Rust

For best performance, most of affinitree is written in the system language Rust.
The corresponding sources can be found at [affinitree (rust)](https://github.com/Conturing/affinitree).
To make the interaction with compiled languages easier, python allows to provide pre-compiled binaries for each target architecture, called *wheels*.

After installing Rust and maturin, wheels for your current architecture can be build using:
```sh
maturin build --release
```

To build wheels optimized for your current system, include the following flag:
```sh
RUSTFLAGS="-C target-cpu=native" maturin build --release
```

An example for setting up a manylinux2014 compatible build environment can be found in the included [Dockerfile](Dockerfile).

# License

Copyright 2022–2024 affinitree developers.

The code in this repository is licensed under the [Apache License, Version 2.0](LICENSE_APACHE). You may not use this project except in compliance with those terms.

Binary applications built from this repository (including wheels) contain dependencies with different license terms, see [license](license.html).

## Contributing

Please feel free to create issues, fork the project or submit pull requests.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "affinitree",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": null,
    "keywords": "affinitree,afftree,distillation,XAI",
    "author": null,
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/5e/94/17500c6872356baa9affd5925e96b7b05d6bbcdb440ba728f95c7c02b949/affinitree-0.21.1.tar.gz",
    "platform": null,
    "description": "# Welcome to Affinitree!\n\n[![PyPi](https://img.shields.io/pypi/v/affinitree)](https://pypi.org/project/affinitree/)\n\nAffinitree is an open-source library to generate faithful surrogate models (decision trees) from pre-trained piece-wise linear neural networks.\nThe core part of the library is implemented in Rust for best performance.\n\nThe opaque nature of neural networks stands in the way of their widespread usage, especially in\nsafety critical domains.\nTo tackle such tasks, it is important to access the semantics of neural networks in a structured way.\nOne promising field of XAI research is concerned with finding alternative models for a given neural network\nthat are more transparent, interpretable, or easier to analyze. \nThis approach is called *model distillation* and the resulting model is called *surrogate model*, *proxy model* or *white-box model*.\n\n`Affinitree` is a library that allows to distil a rectifier neural network into a specialized decision tree.\nIn XAI literature, it is generally accepted that decision trees are one of a few model classes that are\ncomprehensible by humans.\nThis makes decision trees a prime candidate for surrogate models.\nFurther, decision trees lend themselves well for the representation of neural networks with ReLU activation.\nDue to the ReLU activation function the networks are piece-wise linear, a fact that `affinitree` uses to distill the network into a decision tree.\n\nCommonly, surrogate models are only an approximation of the true nature of the neural network.\nIn contrast, `affinitree` provides mathematical sound and correct surrogate models.\nThis is achieved by an holistic symbolic execution of the network.\nThe resulting decision structure is human understandable but size must be controlled.\n\n# Installation\n\n**affinitree** requires Python 3.8 - 3.12.\n\n```sh\npip install affinitree\n```\n\nWheels are currently available for linux (x86_64).\nFor other architectures, see [Build Wheels from Rust](#build-wheels-from-rust).\n\n# First Steps\n\n`Affinitree` provides a high-level API to convert a pre-trained ``pytorch`` model into a decision tree (requires installation of `torch`). \n\n```python \nfrom torch import nn\nfrom affinitree.pytorch import from_pytorch\n\nmodel = nn.Sequential(nn.Linear(7, 5),\n                      nn.ReLU(),\n                      nn.Linear(5, 5),\n                      nn.ReLU(),\n                      nn.Linear(5, 4)\n                     )\n\ndd = from_pytorch(model)\n```\n\nIt may be noted that `affinitree` is independent of any concrete neural network library.\nThe function `from_pytorch` is just a thin wrapper that extracts numpy arrays from pytorch models for convenience.\nAny model expressed as a sequence of numpy matrices can be read (see also the provided [examples](examples)).\n\nAfter distilling the model, one can use the resulting `AffTree` for plotting the decision tree\nin [graphviz's](https://graphviz.org/) DOT language:\n\n```python\ndot_str(dd)\n```\n\nA simple AffTree may look like this (representing the xor function):\n\n<p align=\"center\">\n  <img alt=\"fig:afftree example (at github)\" height=\"300\" src=\"figures/afftree_example.svg\"/>\n</p>\n\n`Affinitree` provides a method to plot the decision boundaries of an ``AffTree`` using `matplotlib`\n\n```python\nfrom affinitree.plot import plot_preimage_partition, LedgerDiscrete\n\n# derive 10 colors from the tab10 color map and position ledgend at the top \nledger = LedgerDiscrete(cmap='tab10', num=10, position='top')\n# map the terminals of dd to one of the 10 colors based on their class\nledger.fit(dd)\n# plot for each terminal of dd the polytope that is implied by the path from the root to the respective terminal\nplot_preimage_partition(dd, ledger, intervals=[(-20., 15.), (-12., 12.)])\n```\nThe ``ledger`` is used to control the coloring and legend of the plot.\nA resulting plot may look like this:\n\n<p align=\"center\">\n  <img alt=\"fig:mnist preimage partition (at github)\" height=\"400\" src=\"figures/mnist_preimage_partition.svg\"/>\n</p>\n\n## Composition and Schemas\n\nMany operations on ``AffTree``s can be implemented using composition.\nFor example, here are a few common functions in the realm of neural networks expressed as ``AffTree``.\n\nReLU (indim=4):\n\n<p align=\"center\">\n    <img alt=\"fig:relu\" height=\"400\" src=\"figures/relu_4.svg\"/>\n</p>\n\nReLU (indim=4) applied only to the first component (partial ReLU / step ReLU):\n\n<p align=\"center\">\n    <img alt=\"fig:partial-relu\" height=\"150\" src=\"figures/partial_relu_4_0.svg\"/>\n</p>\n\nArgmax (indim=4):\n\n<p align=\"center\">\n    <img alt=\"fig:argmax\" height=\"300\" src=\"figures/argmax_4.svg\"/>\n</p>\n\nSchemas are a collection of typical operations that are used in the context of neural networks.\nIn code, on can apply these as follows:\n\n```python\nfrom affinitree import AffTree\nfrom affinitree import schema\n\ndd = AffTree.identity(2)\n\nrelu = schema.ReLU(2)\ndd = dd.compose(relu)\n```\n\nThe following operations are already provided:\n\n```python\nschema.ReLU(dim=n)\nschema.partial_ReLU(dim=n, row=m)\nschema.argmax(dim=n)\nschema.inf_norm(minimum=a, maximum=b)\nschema.class_characterization(dim=n, clazz=c)\n```\n\nThe interface is easily adaptable to additional needs, one just needs to define a function that returns an ``AffTree`` instance that expresses the required piece-wise linear function.\n\n# Build Wheels from Rust\n\nFor best performance, most of affinitree is written in the system language Rust.\nThe corresponding sources can be found at [affinitree (rust)](https://github.com/Conturing/affinitree).\nTo make the interaction with compiled languages easier, python allows to provide pre-compiled binaries for each target architecture, called *wheels*.\n\nAfter installing Rust and maturin, wheels for your current architecture can be build using:\n```sh\nmaturin build --release\n```\n\nTo build wheels optimized for your current system, include the following flag:\n```sh\nRUSTFLAGS=\"-C target-cpu=native\" maturin build --release\n```\n\nAn example for setting up a manylinux2014 compatible build environment can be found in the included [Dockerfile](Dockerfile).\n\n# License\n\nCopyright 2022\u20132024 affinitree developers.\n\nThe code in this repository is licensed under the [Apache License, Version 2.0](LICENSE_APACHE). You may not use this project except in compliance with those terms.\n\nBinary applications built from this repository (including wheels) contain dependencies with different license terms, see [license](license.html).\n\n## Contributing\n\nPlease feel free to create issues, fork the project or submit pull requests.\n\nUnless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.\n",
    "bugtrack_url": null,
    "license": "Apache 2.0",
    "summary": "Distillation of piece-wise linear neural networks into decision trees",
    "version": "0.21.1",
    "project_urls": {
        "repository": "https://github.com/Conturing/affinitree-py"
    },
    "split_keywords": [
        "affinitree",
        "afftree",
        "distillation",
        "xai"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "71bfeb2c90d6201a3e845b847dc6fa9aa1ecbfdb5ccb676d5ab7eb91f3377dd3",
                "md5": "af29584e1e22c916caeeb617e6496915",
                "sha256": "4e25ae36538b89c603cf411ff13c6c4c062dd5454e7b684274ae46475514431a"
            },
            "downloads": -1,
            "filename": "affinitree-0.21.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "has_sig": false,
            "md5_digest": "af29584e1e22c916caeeb617e6496915",
            "packagetype": "bdist_wheel",
            "python_version": "cp310",
            "requires_python": ">=3.7",
            "size": 1257052,
            "upload_time": "2024-03-08T17:04:53",
            "upload_time_iso_8601": "2024-03-08T17:04:53.937702Z",
            "url": "https://files.pythonhosted.org/packages/71/bf/eb2c90d6201a3e845b847dc6fa9aa1ecbfdb5ccb676d5ab7eb91f3377dd3/affinitree-0.21.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "068ef276613df77298e098183a1e030f1099519d952e3bec830225f293676af0",
                "md5": "f5f42a7826e18ddd4a61f4afb76a5c36",
                "sha256": "e15f66bd605550efaa18de815084074a3c2c38642993215d327cd014544b749d"
            },
            "downloads": -1,
            "filename": "affinitree-0.21.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "has_sig": false,
            "md5_digest": "f5f42a7826e18ddd4a61f4afb76a5c36",
            "packagetype": "bdist_wheel",
            "python_version": "cp311",
            "requires_python": ">=3.7",
            "size": 1257053,
            "upload_time": "2024-03-08T17:04:56",
            "upload_time_iso_8601": "2024-03-08T17:04:56.641421Z",
            "url": "https://files.pythonhosted.org/packages/06/8e/f276613df77298e098183a1e030f1099519d952e3bec830225f293676af0/affinitree-0.21.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "0edd329bddc2e30d1f948ad905311b1baf58426488240ca288b8541e13965082",
                "md5": "a5b4f78911b222b769c486d6d093e9f4",
                "sha256": "11738123b7cd8bb66f37f26d6755621b7e854af2415bc7e54cf8205c78b64995"
            },
            "downloads": -1,
            "filename": "affinitree-0.21.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "has_sig": false,
            "md5_digest": "a5b4f78911b222b769c486d6d093e9f4",
            "packagetype": "bdist_wheel",
            "python_version": "cp312",
            "requires_python": ">=3.7",
            "size": 1254978,
            "upload_time": "2024-03-08T17:04:59",
            "upload_time_iso_8601": "2024-03-08T17:04:59.549646Z",
            "url": "https://files.pythonhosted.org/packages/0e/dd/329bddc2e30d1f948ad905311b1baf58426488240ca288b8541e13965082/affinitree-0.21.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "c6b5c4b3d6755770280d1ce6178ddb6e1022a61f0c7484b7c3cbc3383f5c5f9b",
                "md5": "204b76a1e5b64810a75ebd2b0474330d",
                "sha256": "a3cf10ce64224ca1428e4f8438908064c325ebddda5805b492204a8f30b707b1"
            },
            "downloads": -1,
            "filename": "affinitree-0.21.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "has_sig": false,
            "md5_digest": "204b76a1e5b64810a75ebd2b0474330d",
            "packagetype": "bdist_wheel",
            "python_version": "cp38",
            "requires_python": ">=3.7",
            "size": 1257437,
            "upload_time": "2024-03-08T17:04:48",
            "upload_time_iso_8601": "2024-03-08T17:04:48.281700Z",
            "url": "https://files.pythonhosted.org/packages/c6/b5/c4b3d6755770280d1ce6178ddb6e1022a61f0c7484b7c3cbc3383f5c5f9b/affinitree-0.21.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "699c3ecba8b318bdeb43339026dde9a39c3ccb4000f5c7b8d2b6cb1a6255320f",
                "md5": "60a849a7617f7a8c824f8b231d2e69f2",
                "sha256": "f343593a264ad8e04d54544fe82c39425705e2b374158d708b822ced65a6ac63"
            },
            "downloads": -1,
            "filename": "affinitree-0.21.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "has_sig": false,
            "md5_digest": "60a849a7617f7a8c824f8b231d2e69f2",
            "packagetype": "bdist_wheel",
            "python_version": "cp39",
            "requires_python": ">=3.7",
            "size": 1257066,
            "upload_time": "2024-03-08T17:04:51",
            "upload_time_iso_8601": "2024-03-08T17:04:51.162636Z",
            "url": "https://files.pythonhosted.org/packages/69/9c/3ecba8b318bdeb43339026dde9a39c3ccb4000f5c7b8d2b6cb1a6255320f/affinitree-0.21.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "bd6c67c621536dea070c7de69fe0d8632f9ce336bf311975b0f60f148673977c",
                "md5": "c56471a1693a888bf5f63c66eca289dd",
                "sha256": "ca22d1ab87c4d372874fa1e53679e3daa6a2cb98b7cc3d9f6c8ac0a7f6d6d2f8"
            },
            "downloads": -1,
            "filename": "affinitree-0.21.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "has_sig": false,
            "md5_digest": "c56471a1693a888bf5f63c66eca289dd",
            "packagetype": "bdist_wheel",
            "python_version": "pp310",
            "requires_python": ">=3.7",
            "size": 1256804,
            "upload_time": "2024-03-08T17:05:09",
            "upload_time_iso_8601": "2024-03-08T17:05:09.353030Z",
            "url": "https://files.pythonhosted.org/packages/bd/6c/67c621536dea070c7de69fe0d8632f9ce336bf311975b0f60f148673977c/affinitree-0.21.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "d72fb15cf116aa7092a69b74b3a2d1dc2ccddf96b20e80f995922b6515a953ba",
                "md5": "cae03234aa88421854434be27451a208",
                "sha256": "25190a933fb9ffc98f6c31bb5f56505b1ce7bfea17ab6a7d019c1e3c7ed792a1"
            },
            "downloads": -1,
            "filename": "affinitree-0.21.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "has_sig": false,
            "md5_digest": "cae03234aa88421854434be27451a208",
            "packagetype": "bdist_wheel",
            "python_version": "pp38",
            "requires_python": ">=3.7",
            "size": 1257228,
            "upload_time": "2024-03-08T17:05:03",
            "upload_time_iso_8601": "2024-03-08T17:05:03.043986Z",
            "url": "https://files.pythonhosted.org/packages/d7/2f/b15cf116aa7092a69b74b3a2d1dc2ccddf96b20e80f995922b6515a953ba/affinitree-0.21.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "eb204565ddb10373a1cc8b74d874c0571b06f11e0c8aae83f93ebb53f55cbeac",
                "md5": "8cfd581a26dc58eae09d8ba1ee8bee1f",
                "sha256": "6f13b4c8f2d5a214bc85e4b8fa48f8e91c25a45745b5a0b0a2c15589c92c8b7a"
            },
            "downloads": -1,
            "filename": "affinitree-0.21.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "has_sig": false,
            "md5_digest": "8cfd581a26dc58eae09d8ba1ee8bee1f",
            "packagetype": "bdist_wheel",
            "python_version": "pp39",
            "requires_python": ">=3.7",
            "size": 1256808,
            "upload_time": "2024-03-08T17:05:06",
            "upload_time_iso_8601": "2024-03-08T17:05:06.132365Z",
            "url": "https://files.pythonhosted.org/packages/eb/20/4565ddb10373a1cc8b74d874c0571b06f11e0c8aae83f93ebb53f55cbeac/affinitree-0.21.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "5e9417500c6872356baa9affd5925e96b7b05d6bbcdb440ba728f95c7c02b949",
                "md5": "9f3c196108b9f3b350d3db697bce9aa9",
                "sha256": "d46a62c4cc9adc3a93c2339944bb8e4c784c454e80fb83fb8e321bed0e62533e"
            },
            "downloads": -1,
            "filename": "affinitree-0.21.1.tar.gz",
            "has_sig": false,
            "md5_digest": "9f3c196108b9f3b350d3db697bce9aa9",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 45955,
            "upload_time": "2024-03-08T17:05:11",
            "upload_time_iso_8601": "2024-03-08T17:05:11.529411Z",
            "url": "https://files.pythonhosted.org/packages/5e/94/17500c6872356baa9affd5925e96b7b05d6bbcdb440ba728f95c7c02b949/affinitree-0.21.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-08 17:05:11",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "Conturing",
    "github_project": "affinitree-py",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "affinitree"
}
        
Elapsed time: 0.20204s