torchapp


Nametorchapp JSON
Version 0.5.7 PyPI version JSON
download
home_pagehttps://github.com/rbturnbull/torchapp
SummaryA wrapper for PyTorch projects to create easy command-line interfaces and manage hyper-parameter tuning.
upload_time2025-07-16 23:13:10
maintainerNone
docs_urlNone
authorRobert Turnbull
requires_python<3.13,>=3.10
licenseApache-2.0
keywords pytorch pytorch deep learning command-line interface
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage
            ==========
TorchApp
==========

.. image:: https://raw.githubusercontent.com/rbturnbull/torchapp/master/docs/images/torchapp-banner.svg

.. start-badges

|pypi badge| |testing badge| |coverage badge| |docs badge| |black badge| |git3moji badge| |torchapp badge|

.. |pypi badge| image:: https://img.shields.io/pypi/v/torchapp?color=blue
   :alt: PyPI - Version
   :target: https://pypi.org/project/torchapp/

.. |torchapp badge| image:: https://img.shields.io/badge/torch-app-B1230A.svg
    :target: https://rbturnbull.github.io/torchapp/

.. |testing badge| image:: https://github.com/rbturnbull/torchapp/actions/workflows/testing.yml/badge.svg
    :target: https://github.com/rbturnbull/torchapp/actions

.. |docs badge| image:: https://github.com/rbturnbull/torchapp/actions/workflows/docs.yml/badge.svg
    :target: https://rbturnbull.github.io/torchapp
    
.. |black badge| image:: https://img.shields.io/badge/code%20style-black-000000.svg
    :target: https://github.com/psf/black
    
.. |coverage badge| image:: https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/rbturnbull/506563cd9b49c8126284e34864c862d0/raw/coverage-badge.json
    :target: https://rbturnbull.github.io/torchapp/coverage/

.. |git3moji badge| image:: https://img.shields.io/badge/git3moji-%E2%9A%A1%EF%B8%8F%F0%9F%90%9B%F0%9F%93%BA%F0%9F%91%AE%F0%9F%94%A4-fffad8.svg
    :target: https://robinpokorny.github.io/git3moji/

.. end-badges

**A Tool for Packaging PyTorch Models as Reusable Applications**

Deploying machine learning models remains a challenge, particularly in scientific contexts where models are often shared only as standalone scripts or Jupyter notebooks—frequently without parameters and in forms that hinder reuse. TorchApp streamlines this process by enabling users to generate fully structured Python packages from PyTorch models, complete with documentation, testing, and interfaces for training, validation, and prediction. Users subclass the TorchApp base class and override methods to define the model and data loaders; the arguments to these methods are automatically exposed via a command-line interface. TorchApp also supports hyperparameter tuning by allowing argument distributions to be specified, and integrates with experiment tracking tools such as Weights & Biases. Optionally, a graphical user interface can be auto-generated using function signatures and type hints. TorchApp applications are easily testable using a built-in testing framework and are readily publishable to PyPI or Conda, making it simple to share deep learning tools with a broader audience.

Documentation at https://rbturnbull.github.io/torchapp/

.. start-quickstart

Installation
=======================

The software can be installed using ``pip``

.. code-block:: bash

    pip install torchapp

To install the latest version from the repository, you can use this command:

.. code-block:: bash

    pip install git+https://github.com/rbturnbull/torchapp.git


.. warning::

    Earlier versions of torchapp used fastai but current versions use Lightning. 
    If you used torchapp before, please check your code for compatibility with the new version or restrict to using torch below version 0.4.

Writing an App
=======================

Inherit a class from :code:`TorchApp` to make an app. The parent class includes several methods for training and hyper-parameter tuning. 
The minimum requirement is that you fill out the dataloaders method and the model method.

The :code:`data` method requires that you return a ``LightningDataModule`` object. This is a collection of dataloader objects. 
Typically it contains one dataloader for training and another for testing. For more information see https://lightning.ai/docs/pytorch/stable/data/datamodule.html
You can add parameter values with typing hints in the function signature and these will be automatically added to any mehtod that requires the training data (e.g. ``train``).

The :code:`model` method requires that you return a PyTorch module. Parameters in the function signature will be added to the ``train`` method.

Here's an example for doing training on the Iris dataset, a classic dataset for classification tasks:

.. code-block:: Python
   
    from pathlib import Path
    from torch.utils.data import DataLoader, Dataset
    from sklearn.datasets import load_iris
    from torch import nn
    import torchapp as ta
    import torch
    import pandas as pd
    import lightning as L
    from dataclasses import dataclass
    from torchapp.metrics import accuracy


    @dataclass
    class IrisDataset(Dataset):
        df: pd.DataFrame
        feature_names: list[str]

        def __len__(self):
            return len(self.df)

        def __getitem__(self, idx):
            row = self.df.iloc[idx]
            x = torch.tensor(row[self.feature_names].values, dtype=torch.float32)
            y = torch.tensor(row['target'], dtype=int)
            return x, y


    @dataclass
    class Standardize():
        mean:torch.Tensor
        std:torch.Tensor

        def __call__(self, x:torch.Tensor|float) -> torch.Tensor|float:
            return (x - self.mean) / self.std

        def reverse(self, x:torch.Tensor|float) -> torch.Tensor|float:
            return x * self.std + self.mean


    def standardize_and_get_transform(x:torch.Tensor|pd.Series) -> tuple[torch.Tensor|pd.Series, Standardize]:
        transform = Standardize(mean=x.mean(), std=x.std())
        return transform(x), transform


    class IrisApp(ta.TorchApp):
        """
        A classification app to predict the type of iris from sepal and petal lengths and widths.

        A classic dataset publised in:
            Fisher, R.A. “The Use of Multiple Measurements in Taxonomic Problems” Annals of Eugenics, 7, Part II, 179–188 (1936).
        For more information about the dataset, see:
            https://scikit-learn.org/stable/datasets/toy_dataset.html#iris-plants-dataset
        """
        @ta.method
        def setup(self):
            iris_data = load_iris(as_frame=True)
            df = iris_data['frame']
            self.feature_names = iris_data['feature_names']
            self.target_names = iris_data['target_names']
            self.df = df

        @ta.method
        def data(self, validation_fraction: float = 0.2, batch_size: int = 32, seed: int = 42):
            df = self.df

            # Standardize and save the transforms
            self.transforms = {}
            for column in self.feature_names:
                df[column], self.transforms[column] = standardize_and_get_transform(df[column])

            validation_df = df.sample(frac=validation_fraction, random_state=seed)
            train_df = df.drop(validation_df.index)
            train_dataset = IrisDataset(train_df, self.feature_names)
            val_dataset = IrisDataset(validation_df, self.feature_names)
            data_module = L.LightningDataModule()

            data_module.train_dataloader = lambda: DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
            data_module.val_dataloader = lambda: DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
            return data_module

        @ta.method
        def metrics(self):
            return [accuracy]

        @ta.method
        def extra_hyperparameters(self):
            return dict(target_names=self.target_names, transforms=self.transforms)

        @ta.method
        def model(
            self, 
            hidden_size:int=ta.Param(default=8, tune=True, tune_min=4, tune_max=128, tune_log=True),
            intermediate_layers:int=ta.Param(default=1, tune=True, tune_min=0, tune_max=3),
        ):
            in_features = 4
            output_categories = 3

            modules = [nn.Linear(in_features, hidden_size)]
            for _ in range(intermediate_layers):
                modules.append(nn.ReLU())
                modules.append(nn.Linear(hidden_size, hidden_size))

            modules.append(nn.ReLU())
            modules.append(nn.Linear(hidden_size, output_categories))
            return nn.Sequential(*modules)

        @ta.method
        def loss_function(self):
            return nn.CrossEntropyLoss()

        @ta.method
        def get_bibtex_files(self):
            files = super().get_bibtex_files()
            files.append(Path(__file__).parent / "iris.bib")
            return files

        @ta.method
        def prediction_dataloader(
            self, 
            module, 
            sepal_length:float=ta.Param(...,help="The sepal length in cm."), 
            sepal_width:float=ta.Param(...,help="The sepal width in cm."), 
            petal_length:float=ta.Param(...,help="The petal length in cm."), 
            petal_width:float=ta.Param(...,help="The petal width in cm."), 
        ) -> list:
            assert sepal_width is not None
            assert sepal_length is not None
            assert petal_width is not None
            assert petal_length is not None

            self.target_names = module.hparams.target_names

            # data must be in the same order as the feature_names
            data = [sepal_length, sepal_width, petal_length, petal_width]
            transformed_data = [transform(x) for x,transform in zip(data, module.hparams.transforms.values())]
            dataset = [torch.tensor(transformed_data, dtype=torch.float32)]
            return DataLoader(dataset, batch_size=1)

        @ta.method
        def output_results(
            self, 
            results,
        ):
            assert results.shape == (3,)
            probabilities = torch.softmax(results, dim=0)
            predicted_class = results.argmax().item()
            predicted_name = self.target_names[predicted_class]
            print(f"Predicted class: {predicted_name} ({probabilities[predicted_class]:.2%})")
   

Programmatic Interface
=======================

To use the app in Python, simply instantiate it:

.. code-block:: Python

   app = IrisApp()

Then you can train with the method:

.. code-block:: Python

   app.train(csv=training_csv_path)

This takes the arguments of both the :code:`data` method and the :code:`train` method.

Predictions are made by simply calling the app object.

.. code-block:: Python

    app(data_csv_path)

Command-Line Interface
=======================

Command-line interfaces are created simply by using the Poetry package management tool. Just add line like this in :code:`pyproject.toml` (assuming your package is called ``iris``):

.. code-block:: toml

    iris = "iris.apps:IrisApp.main"
    iris-tools = "iris.apps:IrisApp.tools"

Now we can train with the command line:

.. code-block:: bash

    iris-tools train --csv training_csv_path

All the arguments for the dataloader and the model can be set through arguments in the CLI. To see them run

.. code-block:: bash

    iris-tools train --help

Predictions are made like this:

.. code-block:: bash

    iris --csv data_csv_path

See information for other commands by running:

.. code-block:: bash

    iris-tools --help

Hyperparameter Tuning
=======================

All the arguments in the dataloader and the model can be tuned using a variety of hyperparameter tuning libraries including.

In Python run this:

.. code-block:: python

    app.tune(runs=10)

Or from the command line, run

.. code-block:: bash

    iris-tools tune --runs 10

These commands will connect with W&B and your runs will be visible on the wandb.ai site.

Project Generation
=======================

To use a template to construct a package for your app, simply run:

.. code-block:: bash

    torchapp-generator

.. end-quickstart

Credits
=======================

.. start-credits

torchapp was created created by `Robert Turnbull <https://robturnbull.com>`_ with contributions from Wytamma Wirth, Jonathan Garber and Simone Bae.

Citation details to follow.

Logo elements derived from icons by `ProSymbols <https://thenounproject.com/icon/flame-797130/>`_ and `Philipp Petzka <https://thenounproject.com/icon/parcel-2727677/>`_.

.. end-credits
            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/rbturnbull/torchapp",
    "name": "torchapp",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<3.13,>=3.10",
    "maintainer_email": null,
    "keywords": "PyTorch, pytorch, deep learning, command-line interface",
    "author": "Robert Turnbull",
    "author_email": "robert.turnbull@unimelb.edu.au",
    "download_url": "https://files.pythonhosted.org/packages/b7/67/2731b8478e58ecbf03f17ada80d1fd4be15a854c69cf69f88927ccdd7e76/torchapp-0.5.7.tar.gz",
    "platform": null,
    "description": "==========\nTorchApp\n==========\n\n.. image:: https://raw.githubusercontent.com/rbturnbull/torchapp/master/docs/images/torchapp-banner.svg\n\n.. start-badges\n\n|pypi badge| |testing badge| |coverage badge| |docs badge| |black badge| |git3moji badge| |torchapp badge|\n\n.. |pypi badge| image:: https://img.shields.io/pypi/v/torchapp?color=blue\n   :alt: PyPI - Version\n   :target: https://pypi.org/project/torchapp/\n\n.. |torchapp badge| image:: https://img.shields.io/badge/torch-app-B1230A.svg\n    :target: https://rbturnbull.github.io/torchapp/\n\n.. |testing badge| image:: https://github.com/rbturnbull/torchapp/actions/workflows/testing.yml/badge.svg\n    :target: https://github.com/rbturnbull/torchapp/actions\n\n.. |docs badge| image:: https://github.com/rbturnbull/torchapp/actions/workflows/docs.yml/badge.svg\n    :target: https://rbturnbull.github.io/torchapp\n    \n.. |black badge| image:: https://img.shields.io/badge/code%20style-black-000000.svg\n    :target: https://github.com/psf/black\n    \n.. |coverage badge| image:: https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/rbturnbull/506563cd9b49c8126284e34864c862d0/raw/coverage-badge.json\n    :target: https://rbturnbull.github.io/torchapp/coverage/\n\n.. |git3moji badge| image:: https://img.shields.io/badge/git3moji-%E2%9A%A1%EF%B8%8F%F0%9F%90%9B%F0%9F%93%BA%F0%9F%91%AE%F0%9F%94%A4-fffad8.svg\n    :target: https://robinpokorny.github.io/git3moji/\n\n.. end-badges\n\n**A Tool for Packaging PyTorch Models as Reusable Applications**\n\nDeploying machine learning models remains a challenge, particularly in scientific contexts where models are often shared only as standalone scripts or Jupyter notebooks\u2014frequently without parameters and in forms that hinder reuse. TorchApp streamlines this process by enabling users to generate fully structured Python packages from PyTorch models, complete with documentation, testing, and interfaces for training, validation, and prediction. Users subclass the TorchApp base class and override methods to define the model and data loaders; the arguments to these methods are automatically exposed via a command-line interface. TorchApp also supports hyperparameter tuning by allowing argument distributions to be specified, and integrates with experiment tracking tools such as Weights & Biases. Optionally, a graphical user interface can be auto-generated using function signatures and type hints. TorchApp applications are easily testable using a built-in testing framework and are readily publishable to PyPI or Conda, making it simple to share deep learning tools with a broader audience.\n\nDocumentation at https://rbturnbull.github.io/torchapp/\n\n.. start-quickstart\n\nInstallation\n=======================\n\nThe software can be installed using ``pip``\n\n.. code-block:: bash\n\n    pip install torchapp\n\nTo install the latest version from the repository, you can use this command:\n\n.. code-block:: bash\n\n    pip install git+https://github.com/rbturnbull/torchapp.git\n\n\n.. warning::\n\n    Earlier versions of torchapp used fastai but current versions use Lightning. \n    If you used torchapp before, please check your code for compatibility with the new version or restrict to using torch below version 0.4.\n\nWriting an App\n=======================\n\nInherit a class from :code:`TorchApp` to make an app. The parent class includes several methods for training and hyper-parameter tuning. \nThe minimum requirement is that you fill out the dataloaders method and the model method.\n\nThe :code:`data` method requires that you return a ``LightningDataModule`` object. This is a collection of dataloader objects. \nTypically it contains one dataloader for training and another for testing. For more information see https://lightning.ai/docs/pytorch/stable/data/datamodule.html\nYou can add parameter values with typing hints in the function signature and these will be automatically added to any mehtod that requires the training data (e.g. ``train``).\n\nThe :code:`model` method requires that you return a PyTorch module. Parameters in the function signature will be added to the ``train`` method.\n\nHere's an example for doing training on the Iris dataset, a classic dataset for classification tasks:\n\n.. code-block:: Python\n   \n    from pathlib import Path\n    from torch.utils.data import DataLoader, Dataset\n    from sklearn.datasets import load_iris\n    from torch import nn\n    import torchapp as ta\n    import torch\n    import pandas as pd\n    import lightning as L\n    from dataclasses import dataclass\n    from torchapp.metrics import accuracy\n\n\n    @dataclass\n    class IrisDataset(Dataset):\n        df: pd.DataFrame\n        feature_names: list[str]\n\n        def __len__(self):\n            return len(self.df)\n\n        def __getitem__(self, idx):\n            row = self.df.iloc[idx]\n            x = torch.tensor(row[self.feature_names].values, dtype=torch.float32)\n            y = torch.tensor(row['target'], dtype=int)\n            return x, y\n\n\n    @dataclass\n    class Standardize():\n        mean:torch.Tensor\n        std:torch.Tensor\n\n        def __call__(self, x:torch.Tensor|float) -> torch.Tensor|float:\n            return (x - self.mean) / self.std\n\n        def reverse(self, x:torch.Tensor|float) -> torch.Tensor|float:\n            return x * self.std + self.mean\n\n\n    def standardize_and_get_transform(x:torch.Tensor|pd.Series) -> tuple[torch.Tensor|pd.Series, Standardize]:\n        transform = Standardize(mean=x.mean(), std=x.std())\n        return transform(x), transform\n\n\n    class IrisApp(ta.TorchApp):\n        \"\"\"\n        A classification app to predict the type of iris from sepal and petal lengths and widths.\n\n        A classic dataset publised in:\n            Fisher, R.A. \u201cThe Use of Multiple Measurements in Taxonomic Problems\u201d Annals of Eugenics, 7, Part II, 179\u2013188 (1936).\n        For more information about the dataset, see:\n            https://scikit-learn.org/stable/datasets/toy_dataset.html#iris-plants-dataset\n        \"\"\"\n        @ta.method\n        def setup(self):\n            iris_data = load_iris(as_frame=True)\n            df = iris_data['frame']\n            self.feature_names = iris_data['feature_names']\n            self.target_names = iris_data['target_names']\n            self.df = df\n\n        @ta.method\n        def data(self, validation_fraction: float = 0.2, batch_size: int = 32, seed: int = 42):\n            df = self.df\n\n            # Standardize and save the transforms\n            self.transforms = {}\n            for column in self.feature_names:\n                df[column], self.transforms[column] = standardize_and_get_transform(df[column])\n\n            validation_df = df.sample(frac=validation_fraction, random_state=seed)\n            train_df = df.drop(validation_df.index)\n            train_dataset = IrisDataset(train_df, self.feature_names)\n            val_dataset = IrisDataset(validation_df, self.feature_names)\n            data_module = L.LightningDataModule()\n\n            data_module.train_dataloader = lambda: DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\n            data_module.val_dataloader = lambda: DataLoader(val_dataset, batch_size=batch_size, shuffle=False)\n            return data_module\n\n        @ta.method\n        def metrics(self):\n            return [accuracy]\n\n        @ta.method\n        def extra_hyperparameters(self):\n            return dict(target_names=self.target_names, transforms=self.transforms)\n\n        @ta.method\n        def model(\n            self, \n            hidden_size:int=ta.Param(default=8, tune=True, tune_min=4, tune_max=128, tune_log=True),\n            intermediate_layers:int=ta.Param(default=1, tune=True, tune_min=0, tune_max=3),\n        ):\n            in_features = 4\n            output_categories = 3\n\n            modules = [nn.Linear(in_features, hidden_size)]\n            for _ in range(intermediate_layers):\n                modules.append(nn.ReLU())\n                modules.append(nn.Linear(hidden_size, hidden_size))\n\n            modules.append(nn.ReLU())\n            modules.append(nn.Linear(hidden_size, output_categories))\n            return nn.Sequential(*modules)\n\n        @ta.method\n        def loss_function(self):\n            return nn.CrossEntropyLoss()\n\n        @ta.method\n        def get_bibtex_files(self):\n            files = super().get_bibtex_files()\n            files.append(Path(__file__).parent / \"iris.bib\")\n            return files\n\n        @ta.method\n        def prediction_dataloader(\n            self, \n            module, \n            sepal_length:float=ta.Param(...,help=\"The sepal length in cm.\"), \n            sepal_width:float=ta.Param(...,help=\"The sepal width in cm.\"), \n            petal_length:float=ta.Param(...,help=\"The petal length in cm.\"), \n            petal_width:float=ta.Param(...,help=\"The petal width in cm.\"), \n        ) -> list:\n            assert sepal_width is not None\n            assert sepal_length is not None\n            assert petal_width is not None\n            assert petal_length is not None\n\n            self.target_names = module.hparams.target_names\n\n            # data must be in the same order as the feature_names\n            data = [sepal_length, sepal_width, petal_length, petal_width]\n            transformed_data = [transform(x) for x,transform in zip(data, module.hparams.transforms.values())]\n            dataset = [torch.tensor(transformed_data, dtype=torch.float32)]\n            return DataLoader(dataset, batch_size=1)\n\n        @ta.method\n        def output_results(\n            self, \n            results,\n        ):\n            assert results.shape == (3,)\n            probabilities = torch.softmax(results, dim=0)\n            predicted_class = results.argmax().item()\n            predicted_name = self.target_names[predicted_class]\n            print(f\"Predicted class: {predicted_name} ({probabilities[predicted_class]:.2%})\")\n   \n\nProgrammatic Interface\n=======================\n\nTo use the app in Python, simply instantiate it:\n\n.. code-block:: Python\n\n   app = IrisApp()\n\nThen you can train with the method:\n\n.. code-block:: Python\n\n   app.train(csv=training_csv_path)\n\nThis takes the arguments of both the :code:`data` method and the :code:`train` method.\n\nPredictions are made by simply calling the app object.\n\n.. code-block:: Python\n\n    app(data_csv_path)\n\nCommand-Line Interface\n=======================\n\nCommand-line interfaces are created simply by using the Poetry package management tool. Just add line like this in :code:`pyproject.toml` (assuming your package is called ``iris``):\n\n.. code-block:: toml\n\n    iris = \"iris.apps:IrisApp.main\"\n    iris-tools = \"iris.apps:IrisApp.tools\"\n\nNow we can train with the command line:\n\n.. code-block:: bash\n\n    iris-tools train --csv training_csv_path\n\nAll the arguments for the dataloader and the model can be set through arguments in the CLI. To see them run\n\n.. code-block:: bash\n\n    iris-tools train --help\n\nPredictions are made like this:\n\n.. code-block:: bash\n\n    iris --csv data_csv_path\n\nSee information for other commands by running:\n\n.. code-block:: bash\n\n    iris-tools --help\n\nHyperparameter Tuning\n=======================\n\nAll the arguments in the dataloader and the model can be tuned using a variety of hyperparameter tuning libraries including.\n\nIn Python run this:\n\n.. code-block:: python\n\n    app.tune(runs=10)\n\nOr from the command line, run\n\n.. code-block:: bash\n\n    iris-tools tune --runs 10\n\nThese commands will connect with W&B and your runs will be visible on the wandb.ai site.\n\nProject Generation\n=======================\n\nTo use a template to construct a package for your app, simply run:\n\n.. code-block:: bash\n\n    torchapp-generator\n\n.. end-quickstart\n\nCredits\n=======================\n\n.. start-credits\n\ntorchapp was created created by `Robert Turnbull <https://robturnbull.com>`_ with contributions from Wytamma Wirth, Jonathan Garber and Simone Bae.\n\nCitation details to follow.\n\nLogo elements derived from icons by `ProSymbols <https://thenounproject.com/icon/flame-797130/>`_ and `Philipp Petzka <https://thenounproject.com/icon/parcel-2727677/>`_.\n\n.. end-credits",
    "bugtrack_url": null,
    "license": "Apache-2.0",
    "summary": "A wrapper for PyTorch projects to create easy command-line interfaces and manage hyper-parameter tuning.",
    "version": "0.5.7",
    "project_urls": {
        "Documentation": "https://rbturnbull.github.io/torchapp/",
        "Homepage": "https://github.com/rbturnbull/torchapp",
        "Repository": "https://github.com/rbturnbull/torchapp"
    },
    "split_keywords": [
        "pytorch",
        " pytorch",
        " deep learning",
        " command-line interface"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "9c4fee629e86677b74e5ca06dc6c0be890f9816fac954c2ccb795a76eca62800",
                "md5": "0bd9008980cad703a9927bd8db7d50eb",
                "sha256": "94db981582143dc8a76a7d3a20d7d0a6bd8e4a2899de1a71bbcd6e255b2c9cc0"
            },
            "downloads": -1,
            "filename": "torchapp-0.5.7-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "0bd9008980cad703a9927bd8db7d50eb",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<3.13,>=3.10",
            "size": 102335,
            "upload_time": "2025-07-16T23:13:09",
            "upload_time_iso_8601": "2025-07-16T23:13:09.509333Z",
            "url": "https://files.pythonhosted.org/packages/9c/4f/ee629e86677b74e5ca06dc6c0be890f9816fac954c2ccb795a76eca62800/torchapp-0.5.7-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "b7672731b8478e58ecbf03f17ada80d1fd4be15a854c69cf69f88927ccdd7e76",
                "md5": "c1a1f211faec3c1006638d6bbb254f74",
                "sha256": "adcb7a82403fefe3c8612d793b2190cf7b9ee79250af1444b2ab76a56cce031f"
            },
            "downloads": -1,
            "filename": "torchapp-0.5.7.tar.gz",
            "has_sig": false,
            "md5_digest": "c1a1f211faec3c1006638d6bbb254f74",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<3.13,>=3.10",
            "size": 83394,
            "upload_time": "2025-07-16T23:13:10",
            "upload_time_iso_8601": "2025-07-16T23:13:10.941937Z",
            "url": "https://files.pythonhosted.org/packages/b7/67/2731b8478e58ecbf03f17ada80d1fd4be15a854c69cf69f88927ccdd7e76/torchapp-0.5.7.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-07-16 23:13:10",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "rbturnbull",
    "github_project": "torchapp",
    "travis_ci": false,
    "coveralls": true,
    "github_actions": true,
    "lcname": "torchapp"
}
        
Elapsed time: 2.01345s