firedantic


Namefiredantic JSON
Version 0.7.0 PyPI version JSON
download
home_pagehttps://github.com/ioxiocom/firedantic
SummaryPydantic base models for Firestore
upload_time2024-03-27 10:44:51
maintainerNone
docs_urlNone
authorIOXIO Ltd
requires_python<4.0,>=3.8.1
licenseBSD-3-Clause
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Firedantic

[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ioxiocom/firedantic/publish.yaml)](https://github.com/ioxiocom/firedantic/actions/workflows/publish.yaml)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![PyPI](https://img.shields.io/pypi/v/firedantic)](https://pypi.org/project/firedantic/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/firedantic)](https://pypi.org/project/firedantic/)
[![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)

Database models for Firestore using Pydantic base models.

## Installation

The package is available on PyPI:

```bash
pip install firedantic
```

## Usage

In your application you will need to configure the firestore db client and optionally
the collection prefix, which by default is empty.

```python
from os import environ
from unittest.mock import Mock

import google.auth.credentials
from firedantic import configure
from google.cloud.firestore import Client

# Firestore emulator must be running if using locally.
if environ.get("FIRESTORE_EMULATOR_HOST"):
    client = Client(
        project="firedantic-test",
        credentials=Mock(spec=google.auth.credentials.Credentials)
    )
else:
    client = Client()

configure(client, prefix="firedantic-test-")
```

Once that is done, you can start defining your Pydantic models, e.g:

```python
from pydantic import BaseModel

from firedantic import Model

class Owner(BaseModel):
    """Dummy owner Pydantic model."""
    first_name: str
    last_name: str


class Company(Model):
    """Dummy company Firedantic model."""
    __collection__ = "companies"
    company_id: str
    owner: Owner

# Now you can use the model to save it to Firestore
owner = Owner(first_name="John", last_name="Doe")
company = Company(company_id="1234567-8", owner=owner)
company.save()

# Prints out the firestore ID of the Company model
print(company.id)
```

Querying is done via a MongoDB-like `find()`:

```python
from firedantic import Model
import firedantic.operators as op
from google.cloud.firestore import Query

class Product(Model):
    __collection__ = "products"
    product_id: str
    stock: int
    unit_value: int


Product.find({"product_id": "abc-123"})
Product.find({"stock": {">=": 3}})
# or
Product.find({"stock": {op.GTE: 3}})
Product.find({"stock": {">=": 1}}, order_by=[('unit_value', Query.ASCENDING)], limit=25, offset=50)
Product.find(order_by=[('unit_value', Query.ASCENDING), ('stock', Query.DESCENDING)], limit=2)
```

The query operators are found at
[https://firebase.google.com/docs/firestore/query-data/queries#query_operators](https://firebase.google.com/docs/firestore/query-data/queries#query_operators).

### Async usage

Firedantic can also be used in an async way, like this:

```python
import asyncio
from os import environ
from unittest.mock import Mock

import google.auth.credentials
from google.cloud.firestore import AsyncClient

from firedantic import AsyncModel, configure

# Firestore emulator must be running if using locally.
if environ.get("FIRESTORE_EMULATOR_HOST"):
    client = AsyncClient(
        project="firedantic-test",
        credentials=Mock(spec=google.auth.credentials.Credentials),
    )
else:
    client = AsyncClient()

configure(client, prefix="firedantic-test-")


class Person(AsyncModel):
    __collection__ = "persons"
    name: str


async def main():
    alice = Person(name="Alice")
    await alice.save()
    print(f"Saved Alice as {alice.id}")
    bob = Person(name="Bob")
    await bob.save()
    print(f"Saved Bob as {bob.id}")

    found_alice = await Person.find_one({"name": "Alice"})
    print(f"Found Alice: {found_alice.id}")
    assert alice.id == found_alice.id

    found_bob = await Person.get_by_id(bob.id)
    assert bob.id == found_bob.id
    print(f"Found Bob: {found_bob.id}")

    await alice.delete()
    print("Deleted Alice")
    await bob.delete()
    print("Deleted Bob")


if __name__ == "__main__":
    asyncio.run(main())
```

## Subcollections

Subcollections in Firestore are basically dynamically named collections.

Firedantic supports them via the `SubCollection` and `SubModel` classes, by creating
dynamic classes with collection name determined based on the "parent" class it is in
reference to using the `model_for()` method.

```python
from typing import Optional, Type

from firedantic import AsyncModel, AsyncSubCollection, AsyncSubModel, ModelNotFoundError


class UserStats(AsyncSubModel):
    id: Optional[str] = None
    purchases: int = 0

    class Collection(AsyncSubCollection):
        # Can use any properties of the "parent" model
        __collection_tpl__ = "users/{id}/stats"


class User(AsyncModel):
    __collection__ = "users"
    name: str


async def get_user_purchases(user_id: str, period: str = "2021") -> int:
    user = await User.get_by_id(user_id)
    stats_model: Type[UserStats] = UserStats.model_for(user)
    try:
        stats = await stats_model.get_by_id(period)
    except ModelNotFoundError:
        stats = stats_model()
    return stats.purchases

```

## Composite Indexes and TTL Policies

Firedantic has support for defining composite indexes and TTL policies as well as
creating them.

### Composite indexes

Composite indexes of a collection are defined in `__composite_indexes__`, which is a
list of all indexes to be created.

To define an index, you can use `collection_index` or `collection_group_index`,
depending on the query scope of the index. Each of these takes in an arbitrary amount of
tuples, where the first element is the field name and the second is the order
(`ASCENDING`/`DESCENDING`).

The `set_up_composite_indexes` and `async_set_up_composite_indexes` functions are used
to create indexes.

For more details, see the example further down.

### TTL Policies

The field used for the TTL policy should be a datetime field and the name of the field
should be defined in `__ttl_field__`. The `set_up_ttl_policies` and
`async_set_up_ttl_policies` functions are used to set up the policies.

Note: The TTL policies can not be set up in the Firestore emulator.

### Examples

Below are examples (both sync and async) to show how to use Firedantic to set up
composite indexes and TTL policies.

The examples use `async_set_up_composite_indexes_and_ttl_policies` and
`set_up_composite_indexes_and_ttl_policies` functions to set up both composite indexes
and TTL policies. However, you can use separate functions to set up only either one of
them.

#### Composite Index and TTL Policy Example (sync)

```python
from datetime import datetime

from firedantic import (
    collection_index,
    collection_group_index,
    configure,
    get_all_subclasses,
    Model,
    set_up_composite_indexes_and_ttl_policies,
)
from google.cloud.firestore import Client, Query
from google.cloud.firestore_admin_v1 import FirestoreAdminClient


class ExpiringModel(Model):
    __collection__ = "expiringModel"
    __ttl_field__ = "expire"
    __composite_indexes__ = [
        collection_index(("content", Query.ASCENDING), ("expire", Query.DESCENDING)),
        collection_group_index(("content", Query.DESCENDING), ("expire", Query.ASCENDING)),
    ]

    expire: datetime
    content: str


def main():
    configure(Client(), prefix="firedantic-test-")
    set_up_composite_indexes_and_ttl_policies(
        gcloud_project="my-project",
        models=get_all_subclasses(Model),
        client=FirestoreAdminClient(),
    )
    # or use set_up_composite_indexes / set_up_ttl_policies functions separately


if __name__ == "__main__":
    main()
```

#### Composite Index and TTL Policy Example (async)

```python
import asyncio
from datetime import datetime

from firedantic import (
    AsyncModel,
    async_set_up_composite_indexes_and_ttl_policies,
    collection_index,
    collection_group_index,
    configure,
    get_all_subclasses,
)
from google.cloud.firestore import AsyncClient, Query
from google.cloud.firestore_admin_v1.services.firestore_admin import (
    FirestoreAdminAsyncClient,
)


class ExpiringModel(AsyncModel):
    __collection__ = "expiringModel"
    __ttl_field__ = "expire"
    __composite_indexes__ = [
        collection_index(("content", Query.ASCENDING), ("expire", Query.DESCENDING)),
        collection_group_index(("content", Query.DESCENDING), ("expire", Query.ASCENDING)),
    ]

    expire: datetime
    content: str


async def main():
    configure(AsyncClient(), prefix="firedantic-test-")
    await async_set_up_composite_indexes_and_ttl_policies(
        gcloud_project="my-project",
        models=get_all_subclasses(AsyncModel),
        client=FirestoreAdminAsyncClient(),
    )
    # or await async_set_up_composite_indexes / async_set_up_ttl_policies separately


if __name__ == "__main__":
    asyncio.run(main())
```

## Development

PRs are welcome!

To run tests locally, you should run:

```bash
poetry install
poetry run invoke test
```

### Running Firestore emulator

To run the Firestore emulator locally you will need:

- [Firebase CLI](https://firebase.google.com/docs/cli)

To install the `firebase` CLI run:

```bash
npm install -g firebase-tools
```

Run the Firestore emulator with a predictable port:

```bash
./start_emulator.sh
# or on Windows run the .bat file
start_emulator
```

### About sync and async versions of library

Although this library provides both sync and async versions of models, please keep in
mind that you need to explicitly maintain only async version of it. The synchronous
version is generated automatically by invoke task:

```bash
poetry run invoke unasync
```

We decided to go this way in order to:

- make sure both versions have the same API
- reduce human error factor
- avoid working on two code bases at the same time to reduce maintenance effort

Thus, please make sure you don't modify any of files under
[firedantic/\_sync](./firedantic/_sync) and
[firedantic/tests/tests_sync](./firedantic/tests/tests_sync) by hands. `unasync` is also
running as part of pre-commit hooks, but in order to run the latest version of tests you
have to run it manually.

### Generating changelog

After you have increased the version number in [pyproject.toml](pyproject.toml), please
run the following command to generate a changelog placeholder and fill in the relevant
information about the release in [CHANGELOG.md](CHANGELOG.md):

```bash
poetry run invoke make-changelog
```

## License

This code is released under the BSD 3-Clause license. Details in the
[LICENSE](./LICENSE) file.


            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/ioxiocom/firedantic",
    "name": "firedantic",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.8.1",
    "maintainer_email": null,
    "keywords": null,
    "author": "IOXIO Ltd",
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/e8/51/d5c7cbf54e216392415707f71bd491bb013d1989d359c3e1277cc8aabae5/firedantic-0.7.0.tar.gz",
    "platform": null,
    "description": "# Firedantic\n\n[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ioxiocom/firedantic/publish.yaml)](https://github.com/ioxiocom/firedantic/actions/workflows/publish.yaml)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![PyPI](https://img.shields.io/pypi/v/firedantic)](https://pypi.org/project/firedantic/)\n[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/firedantic)](https://pypi.org/project/firedantic/)\n[![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)\n\nDatabase models for Firestore using Pydantic base models.\n\n## Installation\n\nThe package is available on PyPI:\n\n```bash\npip install firedantic\n```\n\n## Usage\n\nIn your application you will need to configure the firestore db client and optionally\nthe collection prefix, which by default is empty.\n\n```python\nfrom os import environ\nfrom unittest.mock import Mock\n\nimport google.auth.credentials\nfrom firedantic import configure\nfrom google.cloud.firestore import Client\n\n# Firestore emulator must be running if using locally.\nif environ.get(\"FIRESTORE_EMULATOR_HOST\"):\n    client = Client(\n        project=\"firedantic-test\",\n        credentials=Mock(spec=google.auth.credentials.Credentials)\n    )\nelse:\n    client = Client()\n\nconfigure(client, prefix=\"firedantic-test-\")\n```\n\nOnce that is done, you can start defining your Pydantic models, e.g:\n\n```python\nfrom pydantic import BaseModel\n\nfrom firedantic import Model\n\nclass Owner(BaseModel):\n    \"\"\"Dummy owner Pydantic model.\"\"\"\n    first_name: str\n    last_name: str\n\n\nclass Company(Model):\n    \"\"\"Dummy company Firedantic model.\"\"\"\n    __collection__ = \"companies\"\n    company_id: str\n    owner: Owner\n\n# Now you can use the model to save it to Firestore\nowner = Owner(first_name=\"John\", last_name=\"Doe\")\ncompany = Company(company_id=\"1234567-8\", owner=owner)\ncompany.save()\n\n# Prints out the firestore ID of the Company model\nprint(company.id)\n```\n\nQuerying is done via a MongoDB-like `find()`:\n\n```python\nfrom firedantic import Model\nimport firedantic.operators as op\nfrom google.cloud.firestore import Query\n\nclass Product(Model):\n    __collection__ = \"products\"\n    product_id: str\n    stock: int\n    unit_value: int\n\n\nProduct.find({\"product_id\": \"abc-123\"})\nProduct.find({\"stock\": {\">=\": 3}})\n# or\nProduct.find({\"stock\": {op.GTE: 3}})\nProduct.find({\"stock\": {\">=\": 1}}, order_by=[('unit_value', Query.ASCENDING)], limit=25, offset=50)\nProduct.find(order_by=[('unit_value', Query.ASCENDING), ('stock', Query.DESCENDING)], limit=2)\n```\n\nThe query operators are found at\n[https://firebase.google.com/docs/firestore/query-data/queries#query_operators](https://firebase.google.com/docs/firestore/query-data/queries#query_operators).\n\n### Async usage\n\nFiredantic can also be used in an async way, like this:\n\n```python\nimport asyncio\nfrom os import environ\nfrom unittest.mock import Mock\n\nimport google.auth.credentials\nfrom google.cloud.firestore import AsyncClient\n\nfrom firedantic import AsyncModel, configure\n\n# Firestore emulator must be running if using locally.\nif environ.get(\"FIRESTORE_EMULATOR_HOST\"):\n    client = AsyncClient(\n        project=\"firedantic-test\",\n        credentials=Mock(spec=google.auth.credentials.Credentials),\n    )\nelse:\n    client = AsyncClient()\n\nconfigure(client, prefix=\"firedantic-test-\")\n\n\nclass Person(AsyncModel):\n    __collection__ = \"persons\"\n    name: str\n\n\nasync def main():\n    alice = Person(name=\"Alice\")\n    await alice.save()\n    print(f\"Saved Alice as {alice.id}\")\n    bob = Person(name=\"Bob\")\n    await bob.save()\n    print(f\"Saved Bob as {bob.id}\")\n\n    found_alice = await Person.find_one({\"name\": \"Alice\"})\n    print(f\"Found Alice: {found_alice.id}\")\n    assert alice.id == found_alice.id\n\n    found_bob = await Person.get_by_id(bob.id)\n    assert bob.id == found_bob.id\n    print(f\"Found Bob: {found_bob.id}\")\n\n    await alice.delete()\n    print(\"Deleted Alice\")\n    await bob.delete()\n    print(\"Deleted Bob\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## Subcollections\n\nSubcollections in Firestore are basically dynamically named collections.\n\nFiredantic supports them via the `SubCollection` and `SubModel` classes, by creating\ndynamic classes with collection name determined based on the \"parent\" class it is in\nreference to using the `model_for()` method.\n\n```python\nfrom typing import Optional, Type\n\nfrom firedantic import AsyncModel, AsyncSubCollection, AsyncSubModel, ModelNotFoundError\n\n\nclass UserStats(AsyncSubModel):\n    id: Optional[str] = None\n    purchases: int = 0\n\n    class Collection(AsyncSubCollection):\n        # Can use any properties of the \"parent\" model\n        __collection_tpl__ = \"users/{id}/stats\"\n\n\nclass User(AsyncModel):\n    __collection__ = \"users\"\n    name: str\n\n\nasync def get_user_purchases(user_id: str, period: str = \"2021\") -> int:\n    user = await User.get_by_id(user_id)\n    stats_model: Type[UserStats] = UserStats.model_for(user)\n    try:\n        stats = await stats_model.get_by_id(period)\n    except ModelNotFoundError:\n        stats = stats_model()\n    return stats.purchases\n\n```\n\n## Composite Indexes and TTL Policies\n\nFiredantic has support for defining composite indexes and TTL policies as well as\ncreating them.\n\n### Composite indexes\n\nComposite indexes of a collection are defined in `__composite_indexes__`, which is a\nlist of all indexes to be created.\n\nTo define an index, you can use `collection_index` or `collection_group_index`,\ndepending on the query scope of the index. Each of these takes in an arbitrary amount of\ntuples, where the first element is the field name and the second is the order\n(`ASCENDING`/`DESCENDING`).\n\nThe `set_up_composite_indexes` and `async_set_up_composite_indexes` functions are used\nto create indexes.\n\nFor more details, see the example further down.\n\n### TTL Policies\n\nThe field used for the TTL policy should be a datetime field and the name of the field\nshould be defined in `__ttl_field__`. The `set_up_ttl_policies` and\n`async_set_up_ttl_policies` functions are used to set up the policies.\n\nNote: The TTL policies can not be set up in the Firestore emulator.\n\n### Examples\n\nBelow are examples (both sync and async) to show how to use Firedantic to set up\ncomposite indexes and TTL policies.\n\nThe examples use `async_set_up_composite_indexes_and_ttl_policies` and\n`set_up_composite_indexes_and_ttl_policies` functions to set up both composite indexes\nand TTL policies. However, you can use separate functions to set up only either one of\nthem.\n\n#### Composite Index and TTL Policy Example (sync)\n\n```python\nfrom datetime import datetime\n\nfrom firedantic import (\n    collection_index,\n    collection_group_index,\n    configure,\n    get_all_subclasses,\n    Model,\n    set_up_composite_indexes_and_ttl_policies,\n)\nfrom google.cloud.firestore import Client, Query\nfrom google.cloud.firestore_admin_v1 import FirestoreAdminClient\n\n\nclass ExpiringModel(Model):\n    __collection__ = \"expiringModel\"\n    __ttl_field__ = \"expire\"\n    __composite_indexes__ = [\n        collection_index((\"content\", Query.ASCENDING), (\"expire\", Query.DESCENDING)),\n        collection_group_index((\"content\", Query.DESCENDING), (\"expire\", Query.ASCENDING)),\n    ]\n\n    expire: datetime\n    content: str\n\n\ndef main():\n    configure(Client(), prefix=\"firedantic-test-\")\n    set_up_composite_indexes_and_ttl_policies(\n        gcloud_project=\"my-project\",\n        models=get_all_subclasses(Model),\n        client=FirestoreAdminClient(),\n    )\n    # or use set_up_composite_indexes / set_up_ttl_policies functions separately\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n#### Composite Index and TTL Policy Example (async)\n\n```python\nimport asyncio\nfrom datetime import datetime\n\nfrom firedantic import (\n    AsyncModel,\n    async_set_up_composite_indexes_and_ttl_policies,\n    collection_index,\n    collection_group_index,\n    configure,\n    get_all_subclasses,\n)\nfrom google.cloud.firestore import AsyncClient, Query\nfrom google.cloud.firestore_admin_v1.services.firestore_admin import (\n    FirestoreAdminAsyncClient,\n)\n\n\nclass ExpiringModel(AsyncModel):\n    __collection__ = \"expiringModel\"\n    __ttl_field__ = \"expire\"\n    __composite_indexes__ = [\n        collection_index((\"content\", Query.ASCENDING), (\"expire\", Query.DESCENDING)),\n        collection_group_index((\"content\", Query.DESCENDING), (\"expire\", Query.ASCENDING)),\n    ]\n\n    expire: datetime\n    content: str\n\n\nasync def main():\n    configure(AsyncClient(), prefix=\"firedantic-test-\")\n    await async_set_up_composite_indexes_and_ttl_policies(\n        gcloud_project=\"my-project\",\n        models=get_all_subclasses(AsyncModel),\n        client=FirestoreAdminAsyncClient(),\n    )\n    # or await async_set_up_composite_indexes / async_set_up_ttl_policies separately\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## Development\n\nPRs are welcome!\n\nTo run tests locally, you should run:\n\n```bash\npoetry install\npoetry run invoke test\n```\n\n### Running Firestore emulator\n\nTo run the Firestore emulator locally you will need:\n\n- [Firebase CLI](https://firebase.google.com/docs/cli)\n\nTo install the `firebase` CLI run:\n\n```bash\nnpm install -g firebase-tools\n```\n\nRun the Firestore emulator with a predictable port:\n\n```bash\n./start_emulator.sh\n# or on Windows run the .bat file\nstart_emulator\n```\n\n### About sync and async versions of library\n\nAlthough this library provides both sync and async versions of models, please keep in\nmind that you need to explicitly maintain only async version of it. The synchronous\nversion is generated automatically by invoke task:\n\n```bash\npoetry run invoke unasync\n```\n\nWe decided to go this way in order to:\n\n- make sure both versions have the same API\n- reduce human error factor\n- avoid working on two code bases at the same time to reduce maintenance effort\n\nThus, please make sure you don't modify any of files under\n[firedantic/\\_sync](./firedantic/_sync) and\n[firedantic/tests/tests_sync](./firedantic/tests/tests_sync) by hands. `unasync` is also\nrunning as part of pre-commit hooks, but in order to run the latest version of tests you\nhave to run it manually.\n\n### Generating changelog\n\nAfter you have increased the version number in [pyproject.toml](pyproject.toml), please\nrun the following command to generate a changelog placeholder and fill in the relevant\ninformation about the release in [CHANGELOG.md](CHANGELOG.md):\n\n```bash\npoetry run invoke make-changelog\n```\n\n## License\n\nThis code is released under the BSD 3-Clause license. Details in the\n[LICENSE](./LICENSE) file.\n\n",
    "bugtrack_url": null,
    "license": "BSD-3-Clause",
    "summary": "Pydantic base models for Firestore",
    "version": "0.7.0",
    "project_urls": {
        "Homepage": "https://github.com/ioxiocom/firedantic",
        "Repository": "https://github.com/ioxiocom/firedantic"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "3f3c0eef48ad5871dc9d30f3ad849d8be1c2ba8658811f1b8d7f2b43b314a718",
                "md5": "b1205a86405af742955020c41ae613b2",
                "sha256": "1cdcaea3cb5e3154668334772a831eacef5eb032c7ff66deec31c59534cc7cde"
            },
            "downloads": -1,
            "filename": "firedantic-0.7.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "b1205a86405af742955020c41ae613b2",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.8.1",
            "size": 35936,
            "upload_time": "2024-03-27T10:44:50",
            "upload_time_iso_8601": "2024-03-27T10:44:50.125289Z",
            "url": "https://files.pythonhosted.org/packages/3f/3c/0eef48ad5871dc9d30f3ad849d8be1c2ba8658811f1b8d7f2b43b314a718/firedantic-0.7.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e851d5c7cbf54e216392415707f71bd491bb013d1989d359c3e1277cc8aabae5",
                "md5": "b86ae2f2344ff0d9278a9824537b43ee",
                "sha256": "79fbea25c233a10cdd6c4c8e92985e5755a49a457ae5fc9287d71d1754551ea0"
            },
            "downloads": -1,
            "filename": "firedantic-0.7.0.tar.gz",
            "has_sig": false,
            "md5_digest": "b86ae2f2344ff0d9278a9824537b43ee",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.8.1",
            "size": 25285,
            "upload_time": "2024-03-27T10:44:51",
            "upload_time_iso_8601": "2024-03-27T10:44:51.893414Z",
            "url": "https://files.pythonhosted.org/packages/e8/51/d5c7cbf54e216392415707f71bd491bb013d1989d359c3e1277cc8aabae5/firedantic-0.7.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-27 10:44:51",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "ioxiocom",
    "github_project": "firedantic",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "firedantic"
}
        
Elapsed time: 0.24190s