diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index ca35b0d..eacd647 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml @@ -65,7 +65,7 @@ jobs: env: DATABASE_URL: "sqlite:///testsuite" run: bash scripts/test.sh - - run: mypy ormar tests + - run: mypy ormar tests benchmarks - name: Upload coverage uses: codecov/codecov-action@v3.1.1 - name: Test & publish code coverage diff --git a/.gitignore b/.gitignore index 26114bb..ca0b707 100644 --- a/.gitignore +++ b/.gitignore @@ -9,10 +9,12 @@ build *.pyc *.log test.db +.vscode/ dist /ormar.egg-info/ site profile.py *.db *.db-journal -*coverage.xml \ No newline at end of file +*coverage.xml +.benchmarks/ diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/benchmarks/conftest.py b/benchmarks/conftest.py new file mode 100644 index 0000000..b5be8da --- /dev/null +++ b/benchmarks/conftest.py @@ -0,0 +1,117 @@ +import asyncio +import random +import string +import time + +import databases +import nest_asyncio +import pytest +import pytest_asyncio +import sqlalchemy + +import ormar +from tests.settings import DATABASE_URL + +nest_asyncio.apply() + + +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() +pytestmark = pytest.mark.asyncio + + +class BaseMeta(ormar.ModelMeta): + metadata = metadata + database = database + + +class Author(ormar.Model): + class Meta(BaseMeta): + tablename = "authors" + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + score: float = ormar.Integer(minimum=0, maximum=100) + + +class AuthorWithManyFields(Author): + year_born: int = ormar.Integer() + year_died: int = ormar.Integer(nullable=True) + birthplace: str = ormar.String(max_length=255) + + +class Publisher(ormar.Model): + class Meta(BaseMeta): + tablename = "publishers" + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + prestige: int = ormar.Integer(minimum=0, maximum=10) + + +class Book(ormar.Model): + class Meta(BaseMeta): + tablename = "books" + + id: int = ormar.Integer(primary_key=True) + author: Author = ormar.ForeignKey(Author, index=True) + publisher: Publisher = ormar.ForeignKey(Publisher, index=True) + title: str = ormar.String(max_length=100) + year: int = ormar.Integer(nullable=True) + + +@pytest.fixture(autouse=True, scope="function") # TODO: fix this to be module +def create_test_database(): + engine = sqlalchemy.create_engine(DATABASE_URL) + metadata.drop_all(engine) + metadata.create_all(engine) + yield + metadata.drop_all(engine) + + +@pytest_asyncio.fixture +async def author(): + author = await Author(name="Author", score=10).save() + return author + + +@pytest_asyncio.fixture +async def publisher(): + publisher = await Publisher(name="Publisher", prestige=random.randint(0, 10)).save() + return publisher + + +@pytest_asyncio.fixture +async def authors_in_db(num_models: int): + authors = [ + Author( + name="".join(random.sample(string.ascii_letters, 5)), + score=random.random() * 100, + ) + for i in range(0, num_models) + ] + await Author.objects.bulk_create(authors) + return await Author.objects.all() + + +@pytest_asyncio.fixture +@pytest.mark.benchmark( + min_rounds=1, timer=time.process_time, disable_gc=True, warmup=False +) +async def aio_benchmark(benchmark, event_loop: asyncio.BaseEventLoop): + def _fixture_wrapper(func): + def _func_wrapper(*args, **kwargs): + if asyncio.iscoroutinefunction(func): + + @benchmark + def benchmarked_func(): + a = event_loop.run_until_complete(func(*args, **kwargs)) + return a + + return benchmarked_func + else: + return benchmark(func, *args, **kwargs) + + return _func_wrapper + + return _fixture_wrapper diff --git a/benchmarks/test_benchmark_aggregate.py b/benchmarks/test_benchmark_aggregate.py new file mode 100644 index 0000000..f876a7a --- /dev/null +++ b/benchmarks/test_benchmark_aggregate.py @@ -0,0 +1,57 @@ +from typing import List + +import pytest + +from benchmarks.conftest import Author + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_count(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def count(): + return await Author.objects.count() + + c = count() + assert c == len(authors_in_db) + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_avg(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def avg(): + return await Author.objects.avg("score") + + average = avg() + assert 0 <= average <= 100 + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_sum(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def sum_(): + return await Author.objects.sum("score") + + s = sum_() + assert 0 <= s <= 100 * num_models + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_min(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def min_(): + return await Author.objects.min("score") + + m = min_() + assert 0 <= m <= 100 + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_max(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def max_(): + return await Author.objects.max("score") + + m = max_() + assert 0 <= m <= 100 diff --git a/benchmarks/test_benchmark_bulk_create.py b/benchmarks/test_benchmark_bulk_create.py new file mode 100644 index 0000000..3869cd6 --- /dev/null +++ b/benchmarks/test_benchmark_bulk_create.py @@ -0,0 +1,26 @@ +import random +import string + +import pytest + +from benchmarks.conftest import Author + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_making_and_inserting_models_in_bulk(aio_benchmark, num_models: int): + @aio_benchmark + async def make_and_insert(num_models: int): + authors = [ + Author( + name="".join(random.sample(string.ascii_letters, 5)), + score=random.random() * 100, + ) + for i in range(0, num_models) + ] + assert len(authors) == num_models + + await Author.objects.bulk_create(authors) + + make_and_insert(num_models) diff --git a/benchmarks/test_benchmark_bulk_update.py b/benchmarks/test_benchmark_bulk_update.py new file mode 100644 index 0000000..7a6bfbf --- /dev/null +++ b/benchmarks/test_benchmark_bulk_update.py @@ -0,0 +1,27 @@ +import random +import string +from typing import List + +import pytest + +from benchmarks.conftest import Author + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_updating_models_in_bulk( + aio_benchmark, num_models: int, authors_in_db: List[Author] +): + starting_first_name = authors_in_db[0].name + + @aio_benchmark + async def update(authors: List[Author]): + await Author.objects.bulk_update(authors) + + for author in authors_in_db: + author.name = "".join(random.sample(string.ascii_letters, 5)) + + update(authors_in_db) + author = await Author.objects.get(id=authors_in_db[0].id) + assert author.name != starting_first_name diff --git a/benchmarks/test_benchmark_create.py b/benchmarks/test_benchmark_create.py new file mode 100644 index 0000000..985fe2f --- /dev/null +++ b/benchmarks/test_benchmark_create.py @@ -0,0 +1,91 @@ +import random +import string + +import pytest + +from benchmarks.conftest import Author, Book, Publisher + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_creating_models_individually(aio_benchmark, num_models: int): + @aio_benchmark + async def create(num_models: int): + authors = [] + for idx in range(0, num_models): + author = await Author.objects.create( + name="".join(random.sample(string.ascii_letters, 5)), + score=random.random() * 100, + ) + authors.append(author) + return authors + + authors = create(num_models) + for author in authors: + assert author.id is not None + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_creating_individually_with_related_models( + aio_benchmark, num_models: int, author: Author, publisher: Publisher +): + @aio_benchmark + async def create_with_related_models( + author: Author, publisher: Publisher, num_models: int + ): + books = [] + for idx in range(0, num_models): + book = await Book.objects.create( + author=author, + publisher=publisher, + title="".join(random.sample(string.ascii_letters, 5)), + year=random.randint(0, 2000), + ) + books.append(book) + + return books + + books = create_with_related_models( + author=author, publisher=publisher, num_models=num_models + ) + + for book in books: + assert book.id is not None + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_get_or_create_when_create(aio_benchmark, num_models: int): + @aio_benchmark + async def get_or_create(num_models: int): + authors = [] + for idx in range(0, num_models): + author, created = await Author.objects.get_or_create( + name="".join(random.sample(string.ascii_letters, 5)), + score=random.random() * 100, + ) + assert created + authors.append(author) + return authors + + authors = get_or_create(num_models) + for author in authors: + assert author.id is not None + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_update_or_create_when_create(aio_benchmark, num_models: int): + @aio_benchmark + async def update_or_create(num_models: int): + authors = [] + for idx in range(0, num_models): + author = await Author.objects.update_or_create( + name="".join(random.sample(string.ascii_letters, 5)), + score=random.random() * 100, + ) + authors.append(author) + return authors + + authors = update_or_create(num_models) + for author in authors: + assert author.id is not None diff --git a/benchmarks/test_benchmark_delete.py b/benchmarks/test_benchmark_delete.py new file mode 100644 index 0000000..a11bc8b --- /dev/null +++ b/benchmarks/test_benchmark_delete.py @@ -0,0 +1,36 @@ +from typing import List + +import pytest + +from benchmarks.conftest import Author + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_deleting_all( + aio_benchmark, num_models: int, authors_in_db: List[Author] +): + @aio_benchmark + async def delete_all(): + await Author.objects.delete(each=True) + + delete_all() + + num = await Author.objects.count() + assert num == 0 + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_deleting_individually( + aio_benchmark, num_models: int, authors_in_db: List[Author] +): + @aio_benchmark + async def delete_one_by_one(authors: List[Author]): + for author in authors: + await Author.objects.filter(id=author.id).delete() + + delete_one_by_one(authors_in_db) + + num = await Author.objects.count() + assert num == 0 diff --git a/benchmarks/test_benchmark_get.py b/benchmarks/test_benchmark_get.py new file mode 100644 index 0000000..773718c --- /dev/null +++ b/benchmarks/test_benchmark_get.py @@ -0,0 +1,102 @@ +import random +import string +from typing import List + +import pytest +import pytest_asyncio + +from benchmarks.conftest import Author, Book, Publisher + +pytestmark = pytest.mark.asyncio + + +@pytest_asyncio.fixture() +async def books(author: Author, publisher: Publisher, num_models: int): + books = [ + Book( + author=author, + publisher=publisher, + title="".join(random.sample(string.ascii_letters, 5)), + year=random.randint(0, 2000), + ) + for _ in range(0, num_models) + ] + await Book.objects.bulk_create(books) + return books + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_get_all(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def get_all(authors: List[Author]): + return await Author.objects.all() + + authors = get_all(authors_in_db) + for idx, author in enumerate(authors_in_db): + assert authors[idx].id == author.id + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_get_all_with_related_models( + aio_benchmark, num_models: int, author: Author, books: List[Book] +): + @aio_benchmark + async def get_with_related(author: Author): + return await Author.objects.select_related("books").all(id=author.id) + + authors = get_with_related(author) + assert len(authors[0].books) == num_models + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_get_one(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def get_one(authors: List[Author]): + return await Author.objects.get(id=authors[0].id) + + author = get_one(authors_in_db) + assert author == authors_in_db[0] + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_get_or_none(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def get_or_none(authors: List[Author]): + return await Author.objects.get_or_none(id=authors[0].id) + + author = get_or_none(authors_in_db) + assert author == authors_in_db[0] + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_get_or_create_when_get( + aio_benchmark, num_models: int, authors_in_db: List[Author] +): + @aio_benchmark + async def get_or_create(authors: List[Author]): + author, created = await Author.objects.get_or_create(id=authors[0].id) + assert not created + return author + + author = get_or_create(authors_in_db) + assert author == authors_in_db[0] + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_first(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def first(): + return await Author.objects.first() + + author = first() + assert author == authors_in_db[0] + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_exists(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def check_exists(authors: List[Author]): + return await Author.objects.filter(id=authors[0].id).exists() + + exists = check_exists(authors_in_db) + assert exists diff --git a/benchmarks/test_benchmark_init.py b/benchmarks/test_benchmark_init.py new file mode 100644 index 0000000..c1a5c01 --- /dev/null +++ b/benchmarks/test_benchmark_init.py @@ -0,0 +1,48 @@ +import random +import string + +import pytest + +from benchmarks.conftest import Author, Book, Publisher + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_initializing_models(aio_benchmark, num_models: int): + @aio_benchmark + async def initialize_models(num_models: int): + authors = [ + Author( + name="".join(random.sample(string.ascii_letters, 5)), + score=random.random() * 100, + ) + for i in range(0, num_models) + ] + assert len(authors) == num_models + + initialize_models(num_models) + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_initializing_models_with_related_models(aio_benchmark, num_models: int): + @aio_benchmark + async def initialize_models_with_related_models( + author: Author, publisher: Publisher, num_models: int + ): + books = [ + Book( + author=author, + publisher=publisher, + title="".join(random.sample(string.ascii_letters, 5)), + year=random.randint(0, 2000), + ) + for i in range(0, num_models) + ] + + author = await Author(name="Author", score=10).save() + publisher = await Publisher(name="Publisher", prestige=random.randint(0, 10)).save() + + ids = initialize_models_with_related_models( + author=author, publisher=publisher, num_models=num_models + ) diff --git a/benchmarks/test_benchmark_iterate.py b/benchmarks/test_benchmark_iterate.py new file mode 100644 index 0000000..2f8f589 --- /dev/null +++ b/benchmarks/test_benchmark_iterate.py @@ -0,0 +1,21 @@ +from typing import List + +import pytest + +from benchmarks.conftest import Author + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_iterate(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def iterate_over_all(authors: List[Author]): + authors = [] + async for author in Author.objects.iterate(): + authors.append(author) + return authors + + authors = iterate_over_all(authors_in_db) + for idx, author in enumerate(authors_in_db): + assert authors[idx].id == author.id diff --git a/benchmarks/test_benchmark_save.py b/benchmarks/test_benchmark_save.py new file mode 100644 index 0000000..333f118 --- /dev/null +++ b/benchmarks/test_benchmark_save.py @@ -0,0 +1,65 @@ +import random +import string + +import pytest + +from benchmarks.conftest import Author, Book, Publisher + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_saving_models_individually(aio_benchmark, num_models: int): + @aio_benchmark + async def make_and_insert(num_models: int): + authors = [ + Author( + name="".join(random.sample(string.ascii_letters, 5)), + score=random.random() * 100, + ) + for i in range(0, num_models) + ] + assert len(authors) == num_models + + ids = [] + for author in authors: + a = await author.save() + ids.append(a) + return ids + + ids = make_and_insert(num_models) + for id in ids: + assert id is not None + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_saving_models_individually_with_related_models( + aio_benchmark, num_models: int, author: Author, publisher: Publisher +): + @aio_benchmark + async def making_and_inserting_related_models_one_by_one( + author: Author, publisher: Publisher, num_models: int + ): + books = [ + Book( + author=author, + publisher=publisher, + title="".join(random.sample(string.ascii_letters, 5)), + year=random.randint(0, 2000), + ) + for i in range(0, num_models) + ] + + ids = [] + for book in books: + await book.save() + ids.append(book.id) + + return ids + + ids = making_and_inserting_related_models_one_by_one( + author=author, publisher=publisher, num_models=num_models + ) + + for id in ids: + assert id is not None diff --git a/benchmarks/test_benchmark_update.py b/benchmarks/test_benchmark_update.py new file mode 100644 index 0000000..b24a937 --- /dev/null +++ b/benchmarks/test_benchmark_update.py @@ -0,0 +1,27 @@ +import random +import string +from typing import List + +import pytest + +from benchmarks.conftest import Author + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.parametrize("num_models", [10, 20, 40]) +async def test_updating_models_individually( + aio_benchmark, num_models: int, authors_in_db: List[Author] +): + starting_first_name = authors_in_db[0].name + + @aio_benchmark + async def update(authors: List[Author]): + for author in authors: + a = await author.update( + name="".join(random.sample(string.ascii_letters, 5)) + ) + + update(authors_in_db) + author = await Author.objects.get(id=authors_in_db[0].id) + assert author.name != starting_first_name diff --git a/benchmarks/test_benchmark_values.py b/benchmarks/test_benchmark_values.py new file mode 100644 index 0000000..f4f2c04 --- /dev/null +++ b/benchmarks/test_benchmark_values.py @@ -0,0 +1,29 @@ +from typing import List + +import pytest + +from benchmarks.conftest import Author + +pytestmark = pytest.mark.asyncio + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_values(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def get_all_values(authors: List[Author]): + return await Author.objects.values() + + authors_list = get_all_values(authors_in_db) + for idx, author in enumerate(authors_in_db): + assert authors_list[idx]["id"] == author.id + + +@pytest.mark.parametrize("num_models", [250, 500, 1000]) +async def test_values_list(aio_benchmark, num_models: int, authors_in_db: List[Author]): + @aio_benchmark + async def get_all_values_list(authors: List[Author]): + return await Author.objects.values_list() + + authors_list = get_all_values_list(authors_in_db) + for idx, author in enumerate(authors_in_db): + assert authors_list[idx][0] == author.id diff --git a/ormar/models/mixins/merge_mixin.py b/ormar/models/mixins/merge_mixin.py index 67c2cb3..7ef2077 100644 --- a/ormar/models/mixins/merge_mixin.py +++ b/ormar/models/mixins/merge_mixin.py @@ -16,6 +16,31 @@ class MergeModelMixin: in the end all parent (main) models should be unique. """ + @classmethod + def _recursive_add(cls, model_group: List["Model"]) -> List["Model"]: + """ + Instead of accumulating the model additions one by one, this recursively adds + the models. E.G. + [1, 2, 3, 4].accumulate_add() would give [3, 3, 4], then [6, 4], then [10] + where this method looks like + [1, 2, 3, 4].recursive_add() gives [[3], [7]], [10] + It's the same number of adds, but it gives better O(N) performance on sublists + """ + if len(model_group) <= 1: + return model_group + + added_values = [] + iterable_group = iter(model_group) + for model in iterable_group: + next_model = next(iterable_group, None) + if next_model is not None: + combined = cls.merge_two_instances(next_model, model) + else: + combined = model + added_values.append(combined) + + return cls._recursive_add(added_values) + @classmethod def merge_instances_list(cls, result_rows: List["Model"]) -> List["Model"]: """ @@ -37,10 +62,7 @@ class MergeModelMixin: grouped_instances.setdefault(model.pk, []).append(model) for group in grouped_instances.values(): - model = group.pop(0) - if group: - for next_model in group: - model = cls.merge_two_instances(next_model, model) + model = cls._recursive_add(group)[0] merged_rows.append(model) return merged_rows diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index 5ea2098..3a4ec03 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -18,6 +18,7 @@ from typing import ( Union, cast, ) +import functools import databases import pydantic @@ -41,6 +42,7 @@ from ormar.models.modelproxy import ModelTableProxy from ormar.models.utils import Extra from ormar.queryset.utils import translate_list_to_dict from ormar.relations.alias_manager import AliasManager +from ormar.relations.relation import Relation from ormar.relations.relation_manager import RelationsManager if TYPE_CHECKING: # pragma no cover @@ -66,7 +68,14 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass the logic concerned with database connection and data persistance. """ - __slots__ = ("_orm_id", "_orm_saved", "_orm", "_pk_column", "__pk_only__") + __slots__ = ( + "_orm_id", + "_orm_saved", + "_orm", + "_pk_column", + "__pk_only__", + "__cached_hash__", + ) if TYPE_CHECKING: # pragma no cover pk: Any @@ -78,6 +87,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass __metadata__: sqlalchemy.MetaData __database__: databases.Database __relation_map__: Optional[List[str]] + __cached_hash__: Optional[int] _orm_relationship_manager: AliasManager _orm: RelationsManager _orm_id: int @@ -171,12 +181,22 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :return: None :rtype: None """ + prev_hash = hash(self) + if hasattr(self, name): object.__setattr__(self, name, value) else: # let pydantic handle errors for unknown fields super().__setattr__(name, value) + # In this case, the hash could have changed, so update it + if name == self.Meta.pkname or self.pk is None: + object.__setattr__(self, "__cached_hash__", None) + new_hash = hash(self) + + if prev_hash != new_hash: + self._update_relation_cache(prev_hash, new_hash) + def __getattr__(self, item: str) -> Any: """ Used only to silence mypy errors for Through models and reverse relations. @@ -213,6 +233,26 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass for name, value in relations.items(): setattr(self, name, value) + def _update_relation_cache(self, prev_hash: int, new_hash: int) -> None: + """ + Update all relation proxy caches with different hash if we have changed + + :param prev_hash: The previous hash to update + :type prev_hash: int + :param new_hash: The hash to update to + :type new_hash: int + """ + def _update_cache(relations: List[Relation], recurse: bool = True) -> None: + for relation in relations: + relation_proxy = relation.get() + + if hasattr(relation_proxy, "update_cache"): + relation_proxy.update_cache(prev_hash, new_hash) # type: ignore + elif recurse and hasattr(relation_proxy, "_orm"): + _update_cache(relation_proxy._orm._relations.values(), recurse=False) # type: ignore + + _update_cache(list(self._orm._relations.values())) + def _internal_set(self, name: str, value: Any) -> None: """ Delegates call to pydantic. @@ -353,6 +393,23 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass return self.__same__(other) return super().__eq__(other) # pragma no cover + def __hash__(self) -> int: + if getattr(self, "__cached_hash__", None) is not None: + return self.__cached_hash__ or 0 + + if self.pk is not None: + ret = hash(str(self.pk) + self.__class__.__name__) + else: + vals = { + k: v + for k, v in self.__dict__.items() + if k not in self.extract_related_names() + } + ret = hash(str(vals) + self.__class__.__name__) + + object.__setattr__(self, "__cached_hash__", ret) + return ret + def __same__(self, other: "NewBaseModel") -> bool: """ Used by __eq__, compares other model to this model. @@ -365,23 +422,12 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :return: result of comparison :rtype: bool """ - return ( - # self._orm_id == other._orm_id - (self.pk == other.pk and self.pk is not None) - or ( - (self.pk is None and other.pk is None) - and { - k: v - for k, v in self.__dict__.items() - if k not in self.extract_related_names() - } - == { - k: v - for k, v in other.__dict__.items() - if k not in other.extract_related_names() - } - ) - ) + if (self.pk is None and other.pk is not None) or ( + self.pk is not None and other.pk is None + ): + return False + else: + return hash(self) == other.__hash__() def _copy_and_set_values( self: "NewBaseModel", values: "DictStrAny", fields_set: "SetStr", *, deep: bool @@ -391,7 +437,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass recursion through related fields. """ self_dict = values - self_dict.update(self.dict()) + self_dict.update(self.dict(exclude_list=True)) return cast( "NewBaseModel", super()._copy_and_set_values( @@ -630,6 +676,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass exclude: Optional[Dict], exclude_primary_keys: bool, exclude_through_models: bool, + exclude_list: bool, ) -> Dict: """ Traverse nested models and converts them into dictionaries. @@ -643,6 +690,8 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :type include: Optional[Dict] :param exclude: fields to exclude :type exclude: Optional[Dict] + :param exclude: whether to exclude lists + :type exclude: bool :return: current model dict with child models converted to dictionaries :rtype: Dict """ @@ -656,6 +705,9 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass try: nested_model = getattr(self, field) if isinstance(nested_model, MutableSequence): + if exclude_list: + continue + dict_instance[field] = self._extract_nested_models_from_list( relation_map=self._skip_ellipsis( # type: ignore relation_map, field, default_return=dict() @@ -695,6 +747,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass exclude_none: bool = False, exclude_primary_keys: bool = False, exclude_through_models: bool = False, + exclude_list: bool = False, relation_map: Dict = None, ) -> "DictStrAny": # noqa: A003' """ @@ -724,6 +777,8 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :type exclude_defaults: bool :param exclude_none: flag to exclude None values - passed to pydantic :type exclude_none: bool + :param exclude_list: flag to exclude lists of nested values models from dict + :type exclude_list: bool :param relation_map: map of the relations to follow to avoid circural deps :type relation_map: Dict :return: @@ -769,6 +824,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass exclude=exclude, # type: ignore exclude_primary_keys=exclude_primary_keys, exclude_through_models=exclude_through_models, + exclude_list=exclude_list, ) # include model properties as fields in dict diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index f14ab65..ae2f040 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -1,3 +1,4 @@ +import asyncio from typing import ( Any, Dict, @@ -173,7 +174,7 @@ class QuerySet(Generic[T]): ) return await query.prefetch_related(models=models, rows=rows) # type: ignore - def _process_query_result_rows(self, rows: List) -> List["T"]: + async def _process_query_result_rows(self, rows: List) -> List["T"]: """ Process database rows and initialize ormar Model from each of the rows. @@ -182,16 +183,19 @@ class QuerySet(Generic[T]): :return: list of models :rtype: List[Model] """ - result_rows = [ - self.model.from_row( - row=row, - select_related=self._select_related, - excludable=self._excludable, - source_model=self.model, - proxy_source_model=self.proxy_source_model, + result_rows = [] + for row in rows: + result_rows.append( + self.model.from_row( + row=row, + select_related=self._select_related, + excludable=self._excludable, + source_model=self.model, + proxy_source_model=self.proxy_source_model, + ) ) - for row in rows - ] + await asyncio.sleep(0) + if result_rows: return self.model.merge_instances_list(result_rows) # type: ignore return cast(List["T"], result_rows) @@ -914,7 +918,7 @@ class QuerySet(Generic[T]): + self.order_bys, ) rows = await self.database.fetch_all(expr) - processed_rows = self._process_query_result_rows(rows) + processed_rows = await self._process_query_result_rows(rows) if self._prefetch_related and processed_rows: processed_rows = await self._prefetch_related_models(processed_rows, rows) self.check_single_result_rows_count(processed_rows) @@ -979,7 +983,7 @@ class QuerySet(Generic[T]): expr = self.build_select_expression() rows = await self.database.fetch_all(expr) - processed_rows = self._process_query_result_rows(rows) + processed_rows = await self._process_query_result_rows(rows) if self._prefetch_related and processed_rows: processed_rows = await self._prefetch_related_models(processed_rows, rows) self.check_single_result_rows_count(processed_rows) @@ -1050,7 +1054,7 @@ class QuerySet(Generic[T]): expr = self.build_select_expression() rows = await self.database.fetch_all(expr) - result_rows = self._process_query_result_rows(rows) + result_rows = await self._process_query_result_rows(rows) if self._prefetch_related and result_rows: result_rows = await self._prefetch_related_models(result_rows, rows) @@ -1098,12 +1102,12 @@ class QuerySet(Generic[T]): rows.append(row) continue - yield self._process_query_result_rows(rows)[0] + yield (await self._process_query_result_rows(rows))[0] last_primary_key = current_primary_key rows = [row] if rows: - yield self._process_query_result_rows(rows)[0] + yield (await self._process_query_result_rows(rows))[0] async def create(self, **kwargs: Any) -> "T": """ @@ -1138,7 +1142,10 @@ class QuerySet(Generic[T]): if not objects: raise ModelListEmptyError("Bulk create objects are empty!") - ready_objects = [obj.prepare_model_to_save(obj.dict()) for obj in objects] + ready_objects = [] + for obj in objects: + ready_objects.append(obj.prepare_model_to_save(obj.dict())) + await asyncio.sleep(0) # Allow context switching to prevent blocking # don't use execute_many, as in databases it's executed in a loop # instead of using execute_many from drivers @@ -1196,6 +1203,7 @@ class QuerySet(Generic[T]): ready_objects.append( {"new_" + k: v for k, v in new_kwargs.items() if k in columns} ) + await asyncio.sleep(0) pk_column = self.model_meta.table.c.get(self.model.get_column_alias(pk_name)) pk_column_name = self.model.get_column_alias(pk_name) diff --git a/ormar/relations/relation.py b/ormar/relations/relation.py index 296053b..072ee22 100644 --- a/ormar/relations/relation.py +++ b/ormar/relations/relation.py @@ -128,15 +128,20 @@ class Relation(Generic[T]): """ if not isinstance(self.related_models, RelationProxy): # pragma nocover raise ValueError("Cannot find existing models in parent relation type") - if self._to_remove: - self._clean_related() - for ind, relation_child in enumerate(self.related_models[:]): + + if child not in self.related_models: + return None + else: + # We need to clear the weakrefs that don't point to anything anymore + # There's an assumption here that if some of the related models went out of scope, + # then they all did, so we can just check the first one try: - if relation_child == child: - return ind - except ReferenceError: # pragma no cover - self._to_remove.add(ind) - return None + self.related_models[0].__repr__.__self__ + return self.related_models.index(child) + except ReferenceError: + missing = self.related_models._get_list_of_missing_weakrefs() + self._to_remove.update(missing) + return self.related_models.index(child) def add(self, child: "Model") -> None: """ @@ -186,6 +191,8 @@ class Relation(Generic[T]): :return: related model/models if set :rtype: Optional[Union[List[Model], Model]] """ + if self._to_remove: + self._clean_related() return self.related_models def __repr__(self) -> str: # pragma no cover diff --git a/ormar/relations/relation_proxy.py b/ormar/relations/relation_proxy.py index 92bcd20..1d11912 100644 --- a/ormar/relations/relation_proxy.py +++ b/ormar/relations/relation_proxy.py @@ -1,4 +1,15 @@ -from typing import Any, Generic, List, Optional, TYPE_CHECKING, Type, TypeVar +from typing import ( + Any, + Dict, + Generic, + List, + Optional, + TYPE_CHECKING, + Set, + Type, + TypeVar, +) +from typing_extensions import SupportsIndex import ormar from ormar.exceptions import NoMatch, RelationshipInstanceError @@ -26,7 +37,6 @@ class RelationProxy(Generic[T], List[T]): field_name: str, data_: Any = None, ) -> None: - super().__init__(data_ or ()) self.relation: "Relation[T]" = relation self.type_: "RelationType" = type_ self.field_name = field_name @@ -36,6 +46,20 @@ class RelationProxy(Generic[T], List[T]): ) self._related_field_name: Optional[str] = None + self._relation_cache: Dict[int, int] = {} + + validated_data = [] + if data_ is not None: + idx = 0 + for d in data_: + try: + self._relation_cache[d.__hash__()] = idx + validated_data.append(d) + idx += 1 + except ReferenceError: + pass + super().__init__(validated_data or ()) + @property def related_field_name(self) -> str: """ @@ -55,6 +79,101 @@ class RelationProxy(Generic[T], List[T]): def __getitem__(self, item: Any) -> "T": # type: ignore return super().__getitem__(item) + def append(self, item: "T") -> None: + """ + Appends an item to the list in place + + :param item: The generic item of the list + :type item: T + """ + idx = len(self) + self._relation_cache[item.__hash__()] = idx + super().append(item) + + def update_cache(self, prev_hash: int, new_hash: int) -> None: + """ + Updates the cache from the old hash to the new one. + This maintains the index cache, which allows O(1) indexing and + existence checks + + :param prev_hash: The hash to update + :type prev_hash: int + :param prev_hash: The new hash to update to + :type new_hash: int + """ + try: + idx = self._relation_cache.pop(prev_hash) + self._relation_cache[new_hash] = idx + except KeyError: + pass + + def index(self, item: T, *args: Any) -> int: + """ + Gets the index of the item in the list + + :param item: The item to get the index of + :type item: "T" + """ + return self._relation_cache[item.__hash__()] + + def _get_list_of_missing_weakrefs(self) -> Set[int]: + """ + Iterates through the list and checks for weakrefs. + + :return: The set of missing weakref indices + :rtype: Set[int] + """ + to_remove = set() + for ind, relation_child in enumerate(self[:]): + try: + relation_child.__repr__.__self__ # type: ignore + except ReferenceError: # pragma no cover + to_remove.add(ind) + + return to_remove + + def pop(self, index: SupportsIndex = 0) -> T: + """ + Pops the index off the list and returns it. By default, + it pops off the element at index 0. + This also clears the value from the relation cache. + + :param index: The index to pop + :type index: SupportsIndex + :return: The item at the provided index + :rtype: "T" + """ + item = self[index] + + # Try to delete it, but do it the long way if weakly-referenced thing doesn't exist + try: + self._relation_cache.pop(item.__hash__()) + except ReferenceError: + for hash_, idx in self._relation_cache.items(): + if idx == index: + self._relation_cache.pop(hash_) + break + + index_int = int(index) + for idx in range(index_int + 1, len(self)): + self._relation_cache[self[idx].__hash__()] -= 1 + + return super().pop(index) + + def __contains__(self, item: object) -> bool: + """ + Checks whether the item exists in self. This relies + on the relation cache, which is a hashmap of values + in the list. It runs in O(1) time. + + :param item: The item to check if the list contains + :type item: object + """ + try: + return item.__hash__() in self._relation_cache + except ReferenceError: + return False + def __getattribute__(self, item: str) -> Any: """ Since some QuerySetProxy methods overwrite builtin list methods we @@ -83,6 +202,7 @@ class RelationProxy(Generic[T], List[T]): return getattr(self.queryset_proxy, item) def _clear(self) -> None: + self._relation_cache.clear() super().clear() def _initialize_queryset(self) -> None: @@ -164,7 +284,10 @@ class RelationProxy(Generic[T], List[T]): child=item, relation_name=self.field_name, ) - super().remove(item) + + index_to_remove = self._relation_cache[item.__hash__()] + self.pop(index_to_remove) + relation_name = self.related_field_name relation = item._orm._get(relation_name) # if relation is None: # pragma nocover @@ -200,6 +323,7 @@ class RelationProxy(Generic[T], List[T]): :param item: child to add to relation :type item: Model """ + new_idx = len(self) relation_name = self.related_field_name await self._owner.signals.pre_relation_add.send( sender=self._owner.__class__, @@ -215,6 +339,7 @@ class RelationProxy(Generic[T], List[T]): else: setattr(item, relation_name, self._owner) await item.upsert() + self._relation_cache[item.__hash__()] = new_idx await self._owner.signals.post_relation_add.send( sender=self._owner.__class__, instance=self._owner, diff --git a/poetry.lock b/poetry.lock index a48a382..bd746b0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -41,7 +41,7 @@ typing_extensions = ">=3.7.2" [[package]] name = "anyio" -version = "3.6.1" +version = "3.6.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "dev" optional = false @@ -55,7 +55,7 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16)"] +trio = ["trio (>=0.16,<0.22)"] [[package]] name = "astpretty" @@ -234,11 +234,11 @@ setuptools = "*" [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" @@ -256,7 +256,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "38.0.3" +version = "38.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = true @@ -312,7 +312,7 @@ python-versions = "*" [[package]] name = "exceptiongroup" -version = "1.0.0rc9" +version = "1.0.4" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false @@ -462,7 +462,7 @@ setuptools = "*" [[package]] name = "flake8-import-order" -version = "0.18.1" +version = "0.18.2" description = "Flake8 and pylama plugin that checks the ordering of import statements." category = "dev" optional = false @@ -507,11 +507,11 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "gitdb" -version = "4.0.9" +version = "4.0.10" description = "Git Object Database" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] smmap = ">=3.0.1,<6" @@ -530,18 +530,19 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\"" [[package]] name = "greenlet" -version = "1.1.3.post0" +version = "2.0.1" description = "Lightweight in-process concurrent programming" category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" [package.extras] -docs = ["Sphinx"] +docs = ["Sphinx", "docutils (<0.18)"] +test = ["faulthandler", "objgraph", "psutil"] [[package]] name = "griffe" -version = "0.22.2" +version = "0.24.1" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." category = "dev" optional = false @@ -549,13 +550,14 @@ python-versions = ">=3.7" [package.dependencies] cached-property = {version = "*", markers = "python_version < \"3.8\""} +colorama = ">=0.4" [package.extras] async = ["aiofiles (>=0.7,<1.0)"] [[package]] name = "identify" -version = "2.5.6" +version = "2.5.9" description = "File identification library for Python" category = "dev" optional = false @@ -574,7 +576,7 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "5.0.0" +version = "5.1.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -769,14 +771,14 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "0.7.1" +version = "0.8.2" description = "A Python handler for mkdocstrings." category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -griffe = ">=0.11.1" +griffe = ">=0.24" mkdocstrings = ">=0.19" [[package]] @@ -828,6 +830,14 @@ category = "main" optional = true python-versions = ">=3.5" +[[package]] +name = "nest-asyncio" +version = "1.5.6" +description = "Patch asyncio to allow nested event loops" +category = "dev" +optional = false +python-versions = ">=3.5" + [[package]] name = "nodeenv" version = "1.7.0" @@ -860,7 +870,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathspec" -version = "0.10.1" +version = "0.10.2" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -868,7 +878,7 @@ python-versions = ">=3.7" [[package]] name = "pbr" -version = "5.10.0" +version = "5.11.0" description = "Python Build Reasonableness" category = "dev" optional = false @@ -876,15 +886,15 @@ python-versions = ">=2.6" [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.5.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] +test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -926,6 +936,14 @@ category = "main" optional = true python-versions = ">=3.6" +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +description = "Get CPU info with pure Python" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pycodestyle" version = "2.7.0" @@ -978,7 +996,7 @@ plugins = ["importlib-metadata"] [[package]] name = "pymdown-extensions" -version = "9.6" +version = "9.9" description = "Extension pack for Python Markdown." category = "dev" optional = false @@ -1046,6 +1064,23 @@ typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +[[package]] +name = "pytest-benchmark" +version = "4.0.0" +description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +py-cpuinfo = "*" +pytest = ">=3.8" + +[package.extras] +aspect = ["aspectlib"] +elasticsearch = ["elasticsearch"] +histogram = ["pygal", "pygaljs"] + [[package]] name = "pytest-cov" version = "4.0.0" @@ -1111,7 +1146,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "setuptools" -version = "65.5.0" +version = "65.6.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false @@ -1119,7 +1154,7 @@ python-versions = ">=3.7" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -1207,7 +1242,7 @@ develop = ["sphinx"] [[package]] name = "stevedore" -version = "3.5.0" +version = "3.5.2" description = "Manage dynamic plugins for Python applications" category = "dev" optional = false @@ -1243,7 +1278,7 @@ python-versions = ">=3.6" [[package]] name = "types-aiofiles" -version = "22.1.0.3" +version = "22.1.0.4" description = "Typing stubs for aiofiles" category = "dev" optional = false @@ -1326,7 +1361,7 @@ python-versions = "*" [[package]] name = "types-urllib3" -version = "1.26.25" +version = "1.26.25.4" description = "Typing stubs for urllib3" category = "dev" optional = false @@ -1342,11 +1377,11 @@ python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.13" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] @@ -1355,20 +1390,20 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.5" +version = "20.17.0" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.5,<1" +distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] @@ -1395,7 +1430,7 @@ test = ["gevent (>=20.6.2)"] [[package]] name = "zipp" -version = "3.9.0" +version = "3.11.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1418,7 +1453,7 @@ sqlite = ["aiosqlite"] [metadata] lock-version = "1.1" python-versions = "^3.7.0" -content-hash = "8827c20c618bcd489a5040134e40141930df221e8ec7c866b25dffea7a14e4d2" +content-hash = "8db4738076689a156205a7a5fd8f4c734f5f413f5ef03079b7496c72c24abac9" [metadata.files] aiomysql = [ @@ -1434,8 +1469,8 @@ aiosqlite = [ {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, ] anyio = [ - {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, - {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, ] astpretty = [ {file = "astpretty-2.1.0-py2.py3-none-any.whl", hash = "sha256:f81f14b5636f7af81fadb1e3c09ca7702ce4615500d9cc6d6829befb2dec2e3c"}, @@ -1608,8 +1643,8 @@ cognitive-complexity = [ {file = "cognitive_complexity-1.3.0.tar.gz", hash = "sha256:a0cfbd47dee0b19f4056f892389f501694b205c3af69fb703cc744541e03dde5"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coverage = [ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, @@ -1664,32 +1699,32 @@ coverage = [ {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] cryptography = [ - {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320"}, - {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c"}, - {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0"}, - {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748"}, - {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146"}, - {file = "cryptography-38.0.3-cp36-abi3-win32.whl", hash = "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0"}, - {file = "cryptography-38.0.3-cp36-abi3-win_amd64.whl", hash = "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220"}, - {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd"}, - {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55"}, - {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b"}, - {file = "cryptography-38.0.3-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36"}, - {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d"}, - {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7"}, - {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249"}, - {file = "cryptography-38.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50"}, - {file = "cryptography-38.0.3-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0"}, - {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8"}, - {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436"}, - {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548"}, - {file = "cryptography-38.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a"}, - {file = "cryptography-38.0.3.tar.gz", hash = "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd"}, + {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70"}, + {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c"}, + {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00"}, + {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0"}, + {file = "cryptography-38.0.4-cp36-abi3-win32.whl", hash = "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744"}, + {file = "cryptography-38.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d"}, + {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353"}, + {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7"}, + {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9"}, + {file = "cryptography-38.0.4.tar.gz", hash = "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290"}, ] databases = [ {file = "databases-0.6.2-py3-none-any.whl", hash = "sha256:ff4010136ac2bb9da2322a2ffda4ef9185ae1c365e5891e52924dd9499d33dc4"}, @@ -1704,8 +1739,8 @@ distlib = [ {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] exceptiongroup = [ - {file = "exceptiongroup-1.0.0rc9-py3-none-any.whl", hash = "sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337"}, - {file = "exceptiongroup-1.0.0rc9.tar.gz", hash = "sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96"}, + {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, + {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, ] fastapi = [ {file = "fastapi-0.85.2-py3-none-any.whl", hash = "sha256:6292db0edd4a11f0d938d6033ccec5f706e9d476958bf33b119e8ddb4e524bde"}, @@ -1747,8 +1782,8 @@ flake8-functions = [ {file = "flake8_functions-0.0.7.tar.gz", hash = "sha256:40584b05d57e5ab185545bcfa08aa0edca52b04646d0df266e2b1667d6437184"}, ] flake8-import-order = [ - {file = "flake8-import-order-0.18.1.tar.gz", hash = "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"}, - {file = "flake8_import_order-0.18.1-py2.py3-none-any.whl", hash = "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543"}, + {file = "flake8-import-order-0.18.2.tar.gz", hash = "sha256:e23941f892da3e0c09d711babbb0c73bc735242e9b216b726616758a920d900e"}, + {file = "flake8_import_order-0.18.2-py2.py3-none-any.whl", hash = "sha256:82ed59f1083b629b030ee9d3928d9e06b6213eb196fe745b3a7d4af2168130df"}, ] flake8-polyfill = [ {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, @@ -1763,96 +1798,90 @@ ghp-import = [ {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] gitdb = [ - {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, - {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, + {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, + {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, ] gitpython = [ {file = "GitPython-3.1.29-py3-none-any.whl", hash = "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f"}, {file = "GitPython-3.1.29.tar.gz", hash = "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd"}, ] greenlet = [ - {file = "greenlet-1.1.3.post0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:949c9061b8c6d3e6e439466a9be1e787208dec6246f4ec5fffe9677b4c19fcc3"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d7815e1519a8361c5ea2a7a5864945906f8e386fa1bc26797b4d443ab11a4589"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9649891ab4153f217f319914455ccf0b86986b55fc0573ce803eb998ad7d6854"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-win32.whl", hash = "sha256:11fc7692d95cc7a6a8447bb160d98671ab291e0a8ea90572d582d57361360f05"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-win_amd64.whl", hash = "sha256:05ae7383f968bba4211b1fbfc90158f8e3da86804878442b4fb6c16ccbcaa519"}, - {file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ccbe7129a282ec5797df0451ca1802f11578be018a32979131065565da89b392"}, - {file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a8b58232f5b72973350c2b917ea3df0bebd07c3c82a0a0e34775fc2c1f857e9"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f6661b58412879a2aa099abb26d3c93e91dedaba55a6394d1fb1512a77e85de9"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c6e942ca9835c0b97814d14f78da453241837419e0d26f7403058e8db3e38f8"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a812df7282a8fc717eafd487fccc5ba40ea83bb5b13eb3c90c446d88dbdfd2be"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a7a6560df073ec9de2b7cb685b199dfd12519bc0020c62db9d1bb522f989fa"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:17a69967561269b691747e7f436d75a4def47e5efcbc3c573180fc828e176d80"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:60839ab4ea7de6139a3be35b77e22e0398c270020050458b3d25db4c7c394df5"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-win_amd64.whl", hash = "sha256:8926a78192b8b73c936f3e87929931455a6a6c6c385448a07b9f7d1072c19ff3"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c6f90234e4438062d6d09f7d667f79edcc7c5e354ba3a145ff98176f974b8132"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814f26b864ed2230d3a7efe0336f5766ad012f94aad6ba43a7c54ca88dd77cba"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fda1139d87ce5f7bd80e80e54f9f2c6fe2f47983f1a6f128c47bf310197deb6"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0643250dd0756f4960633f5359884f609a234d4066686754e834073d84e9b51"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cb863057bed786f6622982fb8b2c122c68e6e9eddccaa9fa98fd937e45ee6c4f"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8c0581077cf2734569f3e500fab09c0ff6a2ab99b1afcacbad09b3c2843ae743"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:695d0d8b5ae42c800f1763c9fce9d7b94ae3b878919379150ee5ba458a460d57"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5662492df0588a51d5690f6578f3bbbd803e7f8d99a99f3bf6128a401be9c269"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:bffba15cff4802ff493d6edcf20d7f94ab1c2aee7cfc1e1c7627c05f1102eee8"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-win32.whl", hash = "sha256:7afa706510ab079fd6d039cc6e369d4535a48e202d042c32e2097f030a16450f"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-win_amd64.whl", hash = "sha256:3a24f3213579dc8459e485e333330a921f579543a5214dbc935bc0763474ece3"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:64e10f303ea354500c927da5b59c3802196a07468332d292aef9ddaca08d03dd"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:eb6ac495dccb1520667cfea50d89e26f9ffb49fa28496dea2b95720d8b45eb54"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:88720794390002b0c8fa29e9602b395093a9a766b229a847e8d88349e418b28a"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39464518a2abe9c505a727af7c0b4efff2cf242aa168be5f0daa47649f4d7ca8"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0914f02fcaa8f84f13b2df4a81645d9e82de21ed95633765dd5cc4d3af9d7403"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96656c5f7c95fc02c36d4f6ef32f4e94bb0b6b36e6a002c21c39785a4eec5f5d"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4f74aa0092602da2069df0bc6553919a15169d77bcdab52a21f8c5242898f519"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3aeac044c324c1a4027dca0cde550bd83a0c0fbff7ef2c98df9e718a5086c194"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-win32.whl", hash = "sha256:fe7c51f8a2ab616cb34bc33d810c887e89117771028e1e3d3b77ca25ddeace04"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:70048d7b2c07c5eadf8393e6398595591df5f59a2f26abc2f81abca09610492f"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:66aa4e9a726b70bcbfcc446b7ba89c8cec40f405e51422c39f42dfa206a96a05"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:025b8de2273d2809f027d347aa2541651d2e15d593bbce0d5f502ca438c54136"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:82a38d7d2077128a017094aff334e67e26194f46bd709f9dcdacbf3835d47ef5"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7d20c3267385236b4ce54575cc8e9f43e7673fc761b069c820097092e318e3b"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8ece5d1a99a2adcb38f69af2f07d96fb615415d32820108cd340361f590d128"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2794eef1b04b5ba8948c72cc606aab62ac4b0c538b14806d9c0d88afd0576d6b"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a8d24eb5cb67996fb84633fdc96dbc04f2d8b12bfcb20ab3222d6be271616b67"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0120a879aa2b1ac5118bce959ea2492ba18783f65ea15821680a256dfad04754"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-win32.whl", hash = "sha256:bef49c07fcb411c942da6ee7d7ea37430f830c482bf6e4b72d92fd506dd3a427"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:62723e7eb85fa52e536e516ee2ac91433c7bb60d51099293671815ff49ed1c21"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d25cdedd72aa2271b984af54294e9527306966ec18963fd032cc851a725ddc1b"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:924df1e7e5db27d19b1359dc7d052a917529c95ba5b8b62f4af611176da7c8ad"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ec615d2912b9ad807afd3be80bf32711c0ff9c2b00aa004a45fd5d5dde7853d9"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0971d37ae0eaf42344e8610d340aa0ad3d06cd2eee381891a10fe771879791f9"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325f272eb997916b4a3fc1fea7313a8adb760934c2140ce13a2117e1b0a8095d"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75afcbb214d429dacdf75e03a1d6d6c5bd1fa9c35e360df8ea5b6270fb2211c"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5c2d21c2b768d8c86ad935e404cc78c30d53dea009609c3ef3a9d49970c864b5"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:467b73ce5dcd89e381292fb4314aede9b12906c18fab903f995b86034d96d5c8"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-win32.whl", hash = "sha256:8149a6865b14c33be7ae760bcdb73548bb01e8e47ae15e013bf7ef9290ca309a"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-win_amd64.whl", hash = "sha256:104f29dd822be678ef6b16bf0035dcd43206a8a48668a6cae4d2fe9c7a7abdeb"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:c8c9301e3274276d3d20ab6335aa7c5d9e5da2009cccb01127bddb5c951f8870"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8415239c68b2ec9de10a5adf1130ee9cb0ebd3e19573c55ba160ff0ca809e012"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:3c22998bfef3fcc1b15694818fc9b1b87c6cc8398198b96b6d355a7bcb8c934e"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa1845944e62f358d63fcc911ad3b415f585612946b8edc824825929b40e59e"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:890f633dc8cb307761ec566bc0b4e350a93ddd77dc172839be122be12bae3e10"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cf37343e43404699d58808e51f347f57efd3010cc7cee134cdb9141bd1ad9ea"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5edf75e7fcfa9725064ae0d8407c849456553a181ebefedb7606bac19aa1478b"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a954002064ee919b444b19c1185e8cce307a1f20600f47d6f4b6d336972c809"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-win32.whl", hash = "sha256:2ccdc818cc106cc238ff7eba0d71b9c77be868fdca31d6c3b1347a54c9b187b2"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-win_amd64.whl", hash = "sha256:91a84faf718e6f8b888ca63d0b2d6d185c8e2a198d2a7322d75c303e7097c8b7"}, - {file = "greenlet-1.1.3.post0.tar.gz", hash = "sha256:f5e09dc5c6e1796969fd4b775ea1417d70e49a5df29aaa8e5d10675d9e11872c"}, + {file = "greenlet-2.0.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:9ed358312e63bf683b9ef22c8e442ef6c5c02973f0c2a939ec1d7b50c974015c"}, + {file = "greenlet-2.0.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4f09b0010e55bec3239278f642a8a506b91034f03a4fb28289a7d448a67f1515"}, + {file = "greenlet-2.0.1-cp27-cp27m-win32.whl", hash = "sha256:1407fe45246632d0ffb7a3f4a520ba4e6051fc2cbd61ba1f806900c27f47706a"}, + {file = "greenlet-2.0.1-cp27-cp27m-win_amd64.whl", hash = "sha256:3001d00eba6bbf084ae60ec7f4bb8ed375748f53aeaefaf2a37d9f0370558524"}, + {file = "greenlet-2.0.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d566b82e92ff2e09dd6342df7e0eb4ff6275a3f08db284888dcd98134dbd4243"}, + {file = "greenlet-2.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:0722c9be0797f544a3ed212569ca3fe3d9d1a1b13942d10dd6f0e8601e484d26"}, + {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d37990425b4687ade27810e3b1a1c37825d242ebc275066cfee8cb6b8829ccd"}, + {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be35822f35f99dcc48152c9839d0171a06186f2d71ef76dc57fa556cc9bf6b45"}, + {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c140e7eb5ce47249668056edf3b7e9900c6a2e22fb0eaf0513f18a1b2c14e1da"}, + {file = "greenlet-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d21681f09e297a5adaa73060737e3aa1279a13ecdcfcc6ef66c292cb25125b2d"}, + {file = "greenlet-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb412b7db83fe56847df9c47b6fe3f13911b06339c2aa02dcc09dce8bbf582cd"}, + {file = "greenlet-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6a08799e9e88052221adca55741bf106ec7ea0710bca635c208b751f0d5b617"}, + {file = "greenlet-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e112e03d37987d7b90c1e98ba5e1b59e1645226d78d73282f45b326f7bddcb9"}, + {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56961cfca7da2fdd178f95ca407fa330c64f33289e1804b592a77d5593d9bd94"}, + {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13ba6e8e326e2116c954074c994da14954982ba2795aebb881c07ac5d093a58a"}, + {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bf633a50cc93ed17e494015897361010fc08700d92676c87931d3ea464123ce"}, + {file = "greenlet-2.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9f2c221eecb7ead00b8e3ddb913c67f75cba078fd1d326053225a3f59d850d72"}, + {file = "greenlet-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:13ebf93c343dd8bd010cd98e617cb4c1c1f352a0cf2524c82d3814154116aa82"}, + {file = "greenlet-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f61d71bbc9b4a3de768371b210d906726535d6ca43506737682caa754b956cd"}, + {file = "greenlet-2.0.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:2d0bac0385d2b43a7bd1d651621a4e0f1380abc63d6fb1012213a401cbd5bf8f"}, + {file = "greenlet-2.0.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:f6327b6907b4cb72f650a5b7b1be23a2aab395017aa6f1adb13069d66360eb3f"}, + {file = "greenlet-2.0.1-cp35-cp35m-win32.whl", hash = "sha256:81b0ea3715bf6a848d6f7149d25bf018fd24554a4be01fcbbe3fdc78e890b955"}, + {file = "greenlet-2.0.1-cp35-cp35m-win_amd64.whl", hash = "sha256:38255a3f1e8942573b067510f9611fc9e38196077b0c8eb7a8c795e105f9ce77"}, + {file = "greenlet-2.0.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:04957dc96669be041e0c260964cfef4c77287f07c40452e61abe19d647505581"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:4aeaebcd91d9fee9aa768c1b39cb12214b30bf36d2b7370505a9f2165fedd8d9"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974a39bdb8c90a85982cdb78a103a32e0b1be986d411303064b28a80611f6e51"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dca09dedf1bd8684767bc736cc20c97c29bc0c04c413e3276e0962cd7aeb148"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c0757db9bd08470ff8277791795e70d0bf035a011a528ee9a5ce9454b6cba2"}, + {file = "greenlet-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5067920de254f1a2dee8d3d9d7e4e03718e8fd2d2d9db962c8c9fa781ae82a39"}, + {file = "greenlet-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5a8e05057fab2a365c81abc696cb753da7549d20266e8511eb6c9d9f72fe3e92"}, + {file = "greenlet-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:3d75b8d013086b08e801fbbb896f7d5c9e6ccd44f13a9241d2bf7c0df9eda928"}, + {file = "greenlet-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:097e3dae69321e9100202fc62977f687454cd0ea147d0fd5a766e57450c569fd"}, + {file = "greenlet-2.0.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:cb242fc2cda5a307a7698c93173d3627a2a90d00507bccf5bc228851e8304963"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:72b00a8e7c25dcea5946692a2485b1a0c0661ed93ecfedfa9b6687bd89a24ef5"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"}, + {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"}, + {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"}, + {file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"}, + {file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"}, + {file = "greenlet-2.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:cd4ccc364cf75d1422e66e247e52a93da6a9b73cefa8cad696f3cbbb75af179d"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c8b1c43e75c42a6cafcc71defa9e01ead39ae80bd733a2608b297412beede68"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"}, + {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"}, + {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"}, + {file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"}, + {file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"}, + {file = "greenlet-2.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b1992ba9d4780d9af9726bbcef6a1db12d9ab1ccc35e5773685a24b7fb2758eb"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b5e83e4de81dcc9425598d9469a624826a0b1211380ac444c7c791d4a2137c19"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"}, + {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"}, + {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"}, + {file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"}, + {file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"}, + {file = "greenlet-2.0.1.tar.gz", hash = "sha256:42e602564460da0e8ee67cb6d7236363ee5e131aa15943b6670e44e5c2ed0f67"}, ] griffe = [ - {file = "griffe-0.22.2-py3-none-any.whl", hash = "sha256:cea5415ac6a92f4a22638e3f1f2e661402bac09fb8e8266936d67185a7e0d0fb"}, - {file = "griffe-0.22.2.tar.gz", hash = "sha256:1408e336a4155392bbd81eed9f2f44bf144e71b9c664e905630affe83bbc088e"}, + {file = "griffe-0.24.1-py3-none-any.whl", hash = "sha256:cfd17f61f3815be5a83f27303cd3db6e9fd9328d4070e4824cd5573763a28961"}, + {file = "griffe-0.24.1.tar.gz", hash = "sha256:acc7e6aac2495ffbfd70b2cdd801fff1299ec3e5efaaad23ccd316b711f1d11d"}, ] identify = [ - {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"}, - {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"}, + {file = "identify-2.5.9-py2.py3-none-any.whl", hash = "sha256:a390fb696e164dbddb047a0db26e57972ae52fbd037ae68797e5ae2f4492485d"}, + {file = "identify-2.5.9.tar.gz", hash = "sha256:906036344ca769539610436e40a684e170c3648b552194980bb7b617a8daeb9f"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] importlib-metadata = [ - {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"}, - {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"}, + {file = "importlib_metadata-5.1.0-py3-none-any.whl", hash = "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"}, + {file = "importlib_metadata-5.1.0.tar.gz", hash = "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -1949,8 +1978,8 @@ mkdocstrings = [ {file = "mkdocstrings-0.19.0.tar.gz", hash = "sha256:efa34a67bad11229d532d89f6836a8a215937548623b64f3698a1df62e01cc3e"}, ] mkdocstrings-python = [ - {file = "mkdocstrings-python-0.7.1.tar.gz", hash = "sha256:c334b382dca202dfa37071c182418a6df5818356a95d54362a2b24822ca3af71"}, - {file = "mkdocstrings_python-0.7.1-py3-none-any.whl", hash = "sha256:a22060bfa374697678e9af4e62b020d990dad2711c98f7a9fac5c0345bef93c7"}, + {file = "mkdocstrings-python-0.8.2.tar.gz", hash = "sha256:b22528b7a7a0589d007eced019d97ad14de4eba4b2b9ba6a013bb66edc74ab43"}, + {file = "mkdocstrings_python-0.8.2-py3-none-any.whl", hash = "sha256:213d9592e66e084a9bd2fa4956d6294a3487c6dc9cc45164058d6317249b7b6e"}, ] mr-proper = [ {file = "mr_proper-0.0.7-py3-none-any.whl", hash = "sha256:74a1b60240c46f10ba518707ef72811a01e5c270da0a78b5dd2dd923d99fdb14"}, @@ -1995,6 +2024,10 @@ mysqlclient = [ {file = "mysqlclient-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dea88c8d3f5a5d9293dfe7f087c16dd350ceb175f2f6631c9cf4caf3e19b7a96"}, {file = "mysqlclient-2.1.1.tar.gz", hash = "sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782"}, ] +nest-asyncio = [ + {file = "nest_asyncio-1.5.6-py3-none-any.whl", hash = "sha256:b9a953fb40dceaa587d109609098db21900182b16440652454a146cffb06e8b8"}, + {file = "nest_asyncio-1.5.6.tar.gz", hash = "sha256:d267cc1ff794403f7df692964d1d2a3fa9418ffea2a3f6859a439ff482fef290"}, +] nodeenv = [ {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, @@ -2055,16 +2088,16 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pathspec = [ - {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, - {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, + {file = "pathspec-0.10.2-py3-none-any.whl", hash = "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5"}, + {file = "pathspec-0.10.2.tar.gz", hash = "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"}, ] pbr = [ - {file = "pbr-5.10.0-py2.py3-none-any.whl", hash = "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf"}, - {file = "pbr-5.10.0.tar.gz", hash = "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a"}, + {file = "pbr-5.11.0-py2.py3-none-any.whl", hash = "sha256:db2317ff07c84c4c63648c9064a79fe9d9f5c7ce85a9099d4b6258b3db83225a"}, + {file = "pbr-5.11.0.tar.gz", hash = "sha256:b97bc6695b2aff02144133c2e7399d5885223d42b7912ffaec2ca3898e673bfe"}, ] platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, + {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"}, + {file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -2147,6 +2180,10 @@ psycopg2-binary = [ {file = "psycopg2_binary-2.9.5-cp39-cp39-win32.whl", hash = "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1"}, {file = "psycopg2_binary-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1"}, ] +py-cpuinfo = [ + {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, + {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, +] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, @@ -2202,8 +2239,8 @@ pygments = [ {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] pymdown-extensions = [ - {file = "pymdown_extensions-9.6-py3-none-any.whl", hash = "sha256:1e36490adc7bfcef1fdb21bb0306e93af99cff8ec2db199bd17e3bf009768c11"}, - {file = "pymdown_extensions-9.6.tar.gz", hash = "sha256:b956b806439bbff10f726103a941266beb03fbe99f897c7d5e774d7170339ad9"}, + {file = "pymdown_extensions-9.9-py3-none-any.whl", hash = "sha256:ac698c15265680db5eb13cd4342abfcde2079ac01e5486028f47a1b41547b859"}, + {file = "pymdown_extensions-9.9.tar.gz", hash = "sha256:0f8fb7b74a37a61cc34e90b2c91865458b713ec774894ffad64353a5fce85cfc"}, ] pymysql = [ {file = "PyMySQL-1.0.2-py3-none-any.whl", hash = "sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641"}, @@ -2221,6 +2258,10 @@ pytest-asyncio = [ {file = "pytest-asyncio-0.20.2.tar.gz", hash = "sha256:32a87a9836298a881c0ec637ebcc952cfe23a56436bdc0d09d1511941dd8a812"}, {file = "pytest_asyncio-0.20.2-py3-none-any.whl", hash = "sha256:07e0abf9e6e6b95894a39f688a4a875d63c2128f76c02d03d16ccbc35bcc0f8a"}, ] +pytest-benchmark = [ + {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"}, + {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"}, +] pytest-cov = [ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, @@ -2237,13 +2278,6 @@ pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -2280,8 +2314,8 @@ requests = [ {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] setuptools = [ - {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, - {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, + {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, + {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -2347,8 +2381,8 @@ stdlib-list = [ {file = "stdlib_list-0.8.0-py3-none-any.whl", hash = "sha256:2ae0712a55b68f3fbbc9e58d6fa1b646a062188f49745b495f94d3310a9fdd3e"}, ] stevedore = [ - {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"}, - {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"}, + {file = "stevedore-3.5.2-py3-none-any.whl", hash = "sha256:fa2630e3d0ad3e22d4914aff2501445815b9a4467a6edc49387c667a38faf5bf"}, + {file = "stevedore-3.5.2.tar.gz", hash = "sha256:cf99f41fc0d5a4f185ca4d3d42b03be9011b0a1ec1a4ea1a282be1b4b306dcc2"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -2385,8 +2419,8 @@ typed-ast = [ {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] types-aiofiles = [ - {file = "types-aiofiles-22.1.0.3.tar.gz", hash = "sha256:9406b93f970e5d647b2183c3a01a253e77693b175b6e31de997192639f6c4955"}, - {file = "types_aiofiles-22.1.0.3-py3-none-any.whl", hash = "sha256:c12668e29307ce06f751dbe02960a9fe6a3e3d9055911ca254839e8e9a78553f"}, + {file = "types-aiofiles-22.1.0.4.tar.gz", hash = "sha256:54465417fdfafc723e6f0fca66de5498866f1cf2595b72c9da35c96d1c3e9fce"}, + {file = "types_aiofiles-22.1.0.4-py3-none-any.whl", hash = "sha256:3c3d389ceec04c78bd33d13304332dcc46856a418d7d4ebf95e398f1efa3a3b7"}, ] types-cryptography = [ {file = "types-cryptography-3.3.23.2.tar.gz", hash = "sha256:09cc53f273dd4d8c29fa7ad11fefd9b734126d467960162397bc5e3e604dea75"}, @@ -2425,20 +2459,20 @@ types-ujson = [ {file = "types_ujson-5.5.0-py3-none-any.whl", hash = "sha256:6051e6b80d71382f1f85b0f97b475d84621eae3e2789ed24c5721f784b0846e1"}, ] types-urllib3 = [ - {file = "types-urllib3-1.26.25.tar.gz", hash = "sha256:5aef0e663724eef924afa8b320b62ffef2c1736c1fa6caecfc9bc6c8ae2c3def"}, - {file = "types_urllib3-1.26.25-py3-none-any.whl", hash = "sha256:c1d78cef7bd581e162e46c20a57b2e1aa6ebecdcf01fd0713bb90978ff3e3427"}, + {file = "types-urllib3-1.26.25.4.tar.gz", hash = "sha256:eec5556428eec862b1ac578fb69aab3877995a99ffec9e5a12cf7fbd0cc9daee"}, + {file = "types_urllib3-1.26.25.4-py3-none-any.whl", hash = "sha256:ed6b9e8a8be488796f72306889a06a3fc3cb1aa99af02ab8afb50144d7317e49"}, ] typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, ] virtualenv = [ - {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, - {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, + {file = "virtualenv-20.17.0-py3-none-any.whl", hash = "sha256:40a7e06a98728fd5769e1af6fd1a706005b4bb7e16176a272ed4292473180389"}, + {file = "virtualenv-20.17.0.tar.gz", hash = "sha256:7d6a8d55b2f73b617f684ee40fd85740f062e1f2e379412cb1879c7136f05902"}, ] watchdog = [ {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"}, @@ -2513,6 +2547,6 @@ yappi = [ {file = "yappi-1.4.0.tar.gz", hash = "sha256:504b5d8fc7433736cb5e257991d2e7f2946019174f1faec7b2fe947881a17fc0"}, ] zipp = [ - {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, - {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, + {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, + {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, ] diff --git a/pyproject.toml b/pyproject.toml index 2f4899d..46a4fdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,6 +112,8 @@ dataclasses = { version = ">=0.6.0,<0.8 || >0.8,<1.0.0" } # Performance testing yappi = "^1.4.0" +pytest-benchmark = "^4.0.0" +nest-asyncio = "^1.5.6" pre-commit = "^2.20.0" @@ -146,7 +148,7 @@ disallow_untyped_defs = true disallow_incomplete_defs = true [[tool.mypy.overrides]] -module = "tests.*" +module = ["tests.*", "benchmarks.*"] disallow_untyped_calls = false disallow_untyped_defs = false disallow_incomplete_defs = false @@ -156,11 +158,10 @@ module = "docs_src.*" ignore_errors = true [[tool.mypy.overrides]] -module = ["sqlalchemy.*", "asyncpg"] +module = ["sqlalchemy.*", "asyncpg", "nest_asyncio"] ignore_missing_imports = true [tool.yapf] based_on_style = "pep8" disable_ending_comma_heuristic = true split_arguments_when_comma_terminated = true - diff --git a/scripts/test.sh b/scripts/test.sh index 5d763a0..f1d6901 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -9,4 +9,4 @@ fi set -x -PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov=${PACKAGE} --cov=tests --cov-report=xml --cov-fail-under=100 --cov-report=term-missing "${@}" +PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov=${PACKAGE} --cov=tests --cov-report=xml --cov-fail-under=100 --cov-report=term-missing tests/ "${@}" diff --git a/tests/test_model_definition/test_equality_and_hash.py b/tests/test_model_definition/test_equality_and_hash.py new file mode 100644 index 0000000..b012166 --- /dev/null +++ b/tests/test_model_definition/test_equality_and_hash.py @@ -0,0 +1,67 @@ +# type: ignore +import databases +import pytest +import sqlalchemy + +import ormar +from ormar import ModelDefinitionError, property_field +from tests.settings import DATABASE_URL + +database = databases.Database(DATABASE_URL, force_rollback=True) +metadata = sqlalchemy.MetaData() + + +class Song(ormar.Model): + class Meta: + tablename = "songs" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + +@pytest.fixture(autouse=True, scope="module") +def create_test_database(): + engine = sqlalchemy.create_engine(DATABASE_URL) + metadata.drop_all(engine) + metadata.create_all(engine) + yield + metadata.drop_all(engine) + + +@pytest.mark.asyncio +async def test_equality(): + async with database: + song1 = await Song.objects.create(name="Song") + song2 = await Song.objects.create(name="Song") + song3 = Song(name="Song") + song4 = Song(name="Song") + + assert song1 == song1 + assert song3 == song4 + + assert song1 != song2 + assert song1 != song3 + assert song3 != song1 + assert song1 is not None + + +@pytest.mark.asyncio +async def test_hash_doesnt_change_with_fields_if_pk(): + async with database: + song1 = await Song.objects.create(name="Song") + prev_hash = hash(song1) + + await song1.update(name="Song 2") + assert hash(song1) == prev_hash + + +@pytest.mark.asyncio +async def test_hash_changes_with_fields_if_no_pk(): + async with database: + song1 = Song(name="Song") + prev_hash = hash(song1) + + song1.name = "Song 2" + assert hash(song1) != prev_hash diff --git a/tests/test_relations/test_weakref_checking.py b/tests/test_relations/test_weakref_checking.py new file mode 100644 index 0000000..dded7fc --- /dev/null +++ b/tests/test_relations/test_weakref_checking.py @@ -0,0 +1,52 @@ +from typing import Optional, Type + +import databases +import pytest +import pytest_asyncio +import sqlalchemy + +import ormar +from tests.settings import DATABASE_URL + +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() + + +class Band(ormar.Model): + class Meta: + tablename = "bands" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + +class Artist(ormar.Model): + class Meta: + tablename = "artists" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + band: Band = ormar.ForeignKey(Band) + + +def test_weakref_init(): + band = Band(name="Band") + artist1 = Artist(name="Artist 1", band=band) + artist2 = Artist(name="Artist 2", band=band) + artist3 = Artist(name="Artist 3", band=band) + + del artist1 + Artist( + name="Artist 2", band=band + ) # Force it to check for weakly-referenced objects + del artist3 + + band.artists # Force it to clean + + assert len(band.artists) == 1 + assert band.artists[0].name == "Artist 2"