Basic testing infrastructure
This commit is contained in:
@ -91,6 +91,7 @@ class TestingSettings(BaseEnvSettings):
|
|||||||
|
|
||||||
DB_HOST: str = "localhost"
|
DB_HOST: str = "localhost"
|
||||||
DB_PORT: int = 5432
|
DB_PORT: int = 5432
|
||||||
|
DB_TEMPLATE_NAME: str = "db-template-name"
|
||||||
DB_NAME: str = "test_db-name"
|
DB_NAME: str = "test_db-name"
|
||||||
DB_USER: str = "db-user"
|
DB_USER: str = "db-user"
|
||||||
DB_PASSWORD: str = "db-password"
|
DB_PASSWORD: str = "db-password"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, cast
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@ -13,7 +14,12 @@ from litestar.contrib.sqlalchemy.plugins.init.config.common import (
|
|||||||
)
|
)
|
||||||
from litestar.utils import delete_litestar_scope_state, get_litestar_scope_state
|
from litestar.utils import delete_litestar_scope_state, get_litestar_scope_state
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import (
|
||||||
|
AsyncEngine,
|
||||||
|
AsyncSession,
|
||||||
|
async_sessionmaker,
|
||||||
|
create_async_engine,
|
||||||
|
)
|
||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
|
|
||||||
from app.lib import settings
|
from app.lib import settings
|
||||||
@ -37,16 +43,25 @@ def _default(val: Any) -> str:
|
|||||||
raise TypeError()
|
raise TypeError()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DBConnectionSettings:
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
database: str
|
||||||
|
|
||||||
|
|
||||||
|
def create_db_engine(connection_settings: DBConnectionSettings) -> AsyncEngine:
|
||||||
db_connection_url = sqlalchemy.engine.URL.create(
|
db_connection_url = sqlalchemy.engine.URL.create(
|
||||||
drivername="postgresql+asyncpg",
|
drivername="postgresql+asyncpg",
|
||||||
username=settings.db.USER,
|
username=connection_settings.username,
|
||||||
password=settings.db.PASSWORD,
|
password=connection_settings.password,
|
||||||
host=settings.db.HOST,
|
host=connection_settings.host,
|
||||||
port=settings.db.PORT,
|
port=connection_settings.port,
|
||||||
database=settings.db.NAME,
|
database=connection_settings.database,
|
||||||
)
|
)
|
||||||
|
return create_async_engine(
|
||||||
engine = create_async_engine(
|
|
||||||
db_connection_url,
|
db_connection_url,
|
||||||
echo=settings.db.ECHO,
|
echo=settings.db.ECHO,
|
||||||
echo_pool=settings.db.ECHO_POOL,
|
echo_pool=settings.db.ECHO_POOL,
|
||||||
@ -56,6 +71,19 @@ engine = create_async_engine(
|
|||||||
pool_timeout=settings.db.POOL_TIMEOUT,
|
pool_timeout=settings.db.POOL_TIMEOUT,
|
||||||
poolclass=NullPool if settings.db.POOL_DISABLE else None,
|
poolclass=NullPool if settings.db.POOL_DISABLE else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
engine = create_db_engine(
|
||||||
|
connection_settings=DBConnectionSettings(
|
||||||
|
username=settings.db.USER,
|
||||||
|
password=settings.db.PASSWORD,
|
||||||
|
host=settings.db.HOST,
|
||||||
|
port=settings.db.PORT,
|
||||||
|
database=settings.db.NAME,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
"""Configure via DatabaseSettings.
|
"""Configure via DatabaseSettings.
|
||||||
|
|
||||||
Overrides default JSON
|
Overrides default JSON
|
||||||
|
|||||||
0
app/lib/test_extras/__init__.py
Normal file
0
app/lib/test_extras/__init__.py
Normal file
107
app/lib/test_extras/db_plugins.py
Normal file
107
app/lib/test_extras/db_plugins.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
from asyncio import current_task
|
||||||
|
from typing import AsyncGenerator, Callable, Generator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from _pytest.fixtures import FixtureRequest
|
||||||
|
from _pytest.tmpdir import TempPathFactory
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_scoped_session
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
import settings_test
|
||||||
|
from televend_core.databases.televend_repositories.mapper import start_televend_mappers
|
||||||
|
from televend_core.databases.televend_repositories.repository import (
|
||||||
|
TelevendRepositoryManager,
|
||||||
|
)
|
||||||
|
from televend_core.test_extras import factory_boy_utils
|
||||||
|
from televend_core.test_extras.database_utils import (
|
||||||
|
DatabaseConfig,
|
||||||
|
configure_database,
|
||||||
|
generate_async_engine,
|
||||||
|
generate_test_database_name,
|
||||||
|
xdist_lock_pytest,
|
||||||
|
)
|
||||||
|
|
||||||
|
TELEVEND_TEMPLATE_DATABASE_CONFIG = DatabaseConfig(
|
||||||
|
engine="postgresql",
|
||||||
|
user=settings_test.POSTGRES_TEST_USER,
|
||||||
|
password=settings_test.POSTGRES_TEST_PASSWORD,
|
||||||
|
name="test_televend",
|
||||||
|
host=settings_test.POSTGRES_HOST,
|
||||||
|
port=settings_test.POSTGRES_PORT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def televend_fixture_setup(worker_id: int, tmp_path_factory: TempPathFactory) -> None:
|
||||||
|
start_televend_mappers("selectin")
|
||||||
|
|
||||||
|
# get the temp directory shared by all workers
|
||||||
|
# When xdist is disabled (running with -n0 for example), then worker_id will return "master"
|
||||||
|
if worker_id == "master":
|
||||||
|
root_tmp_dir = tmp_path_factory.getbasetemp()
|
||||||
|
else:
|
||||||
|
root_tmp_dir = tmp_path_factory.getbasetemp().parent
|
||||||
|
|
||||||
|
file_path = root_tmp_dir / "televend_fixture.txt"
|
||||||
|
lock_path = file_path.with_suffix(".lock")
|
||||||
|
|
||||||
|
xdist_lock_pytest(
|
||||||
|
file_path=file_path,
|
||||||
|
lock_path=lock_path,
|
||||||
|
fn=lambda: configure_database(
|
||||||
|
connection_string=TELEVEND_TEMPLATE_DATABASE_CONFIG.to_connection_string(),
|
||||||
|
database_type="cloud",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def televend_test_database_name(request: FixtureRequest) -> Generator[str, None, None]:
|
||||||
|
test_database_name = generate_test_database_name(test_name=request.node.originalname)
|
||||||
|
yield test_database_name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
async def async_televend_engine(
|
||||||
|
televend_fixture_setup: Callable, televend_test_database_name: str
|
||||||
|
) -> AsyncGenerator[AsyncEngine, None]:
|
||||||
|
async for async_engine in generate_async_engine(
|
||||||
|
template_database_config=TELEVEND_TEMPLATE_DATABASE_CONFIG,
|
||||||
|
test_database_name=televend_test_database_name,
|
||||||
|
):
|
||||||
|
yield async_engine
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
async def async_televend_session(
|
||||||
|
async_televend_engine: AsyncEngine,
|
||||||
|
) -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
# Prepare a new, clean async_session, one sync async_session for factory boy and one async for normal usage
|
||||||
|
sync_engine = create_engine(
|
||||||
|
url=async_televend_engine.url.set(drivername="postgresql"), pool_size=1, max_overflow=1
|
||||||
|
)
|
||||||
|
factory_boy_utils.TelevendSession.configure(bind=sync_engine)
|
||||||
|
|
||||||
|
televend_async_session_factory = sessionmaker(
|
||||||
|
bind=async_televend_engine, expire_on_commit=False, class_=AsyncSession
|
||||||
|
)
|
||||||
|
televend_async_session = async_scoped_session(
|
||||||
|
televend_async_session_factory, scopefunc=current_task
|
||||||
|
)
|
||||||
|
session = televend_async_session()
|
||||||
|
yield session
|
||||||
|
|
||||||
|
factory_boy_utils.TelevendSession.remove()
|
||||||
|
await session.rollback() # to avoid coroutine 'Transaction.rollback' was never awaited warning
|
||||||
|
await televend_async_session.remove()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def televend_repository_manager(
|
||||||
|
async_televend_session: AsyncSession, async_televend_engine: AsyncEngine
|
||||||
|
) -> TelevendRepositoryManager:
|
||||||
|
return TelevendRepositoryManager(
|
||||||
|
async_session=async_televend_session,
|
||||||
|
async_engine=async_televend_engine,
|
||||||
|
)
|
||||||
93
app/lib/test_extras/db_setup.py
Normal file
93
app/lib/test_extras/db_setup.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
import sqlalchemy
|
||||||
|
from asyncpg import Connection, DuplicateDatabaseError, InvalidCatalogNameError
|
||||||
|
|
||||||
|
from migrate import DatabaseConfig, migrate
|
||||||
|
|
||||||
|
|
||||||
|
class TestingSettingsInitOptions(Protocol):
|
||||||
|
DB_HOST: str
|
||||||
|
DB_PORT: int
|
||||||
|
DB_TEMPLATE_NAME: str
|
||||||
|
DB_NAME: str
|
||||||
|
DB_USER: str
|
||||||
|
DB_PASSWORD: str
|
||||||
|
DROP_DATABASE_BEFORE_TESTS: bool
|
||||||
|
DROP_DATABASE_AFTER_TESTS: bool
|
||||||
|
|
||||||
|
|
||||||
|
class TestingDatabaseSetup:
|
||||||
|
def __init__(self, options: TestingSettingsInitOptions):
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
db_connection_url = sqlalchemy.engine.URL.create(
|
||||||
|
drivername="postgresql",
|
||||||
|
username=self.options.DB_USER,
|
||||||
|
password=self.options.DB_PASSWORD,
|
||||||
|
host=self.options.DB_HOST,
|
||||||
|
port=self.options.DB_PORT,
|
||||||
|
)
|
||||||
|
self.connection_str = db_connection_url.render_as_string(hide_password=False)
|
||||||
|
|
||||||
|
async def _create_template_db(self, conn: Connection):
|
||||||
|
query = f"CREATE DATABASE {self.options.DB_TEMPLATE_NAME}"
|
||||||
|
try:
|
||||||
|
await conn.execute(query)
|
||||||
|
except DuplicateDatabaseError:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def _drop_template_db(self, conn: Connection):
|
||||||
|
query = f"DROP DATABASE {self.options.DB_TEMPLATE_NAME}"
|
||||||
|
try:
|
||||||
|
await conn.execute(query)
|
||||||
|
except InvalidCatalogNameError:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def _create_test_db(self, conn: Connection):
|
||||||
|
query = f"CREATE DATABASE {self.options.DB_NAME} TEMPLATE {self.options.DB_TEMPLATE_NAME}"
|
||||||
|
try:
|
||||||
|
await conn.execute(query)
|
||||||
|
except DuplicateDatabaseError:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def _drop_test_db(self, conn: Connection):
|
||||||
|
query = f"DROP DATABASE {self.options.DB_NAME}"
|
||||||
|
try:
|
||||||
|
await conn.execute(query)
|
||||||
|
except InvalidCatalogNameError:
|
||||||
|
...
|
||||||
|
|
||||||
|
def _migrate_template_database(self):
|
||||||
|
conf = DatabaseConfig(
|
||||||
|
HOST=self.options.DB_HOST,
|
||||||
|
PORT=self.options.DB_PORT,
|
||||||
|
NAME=self.options.DB_TEMPLATE_NAME,
|
||||||
|
USER=self.options.DB_USER,
|
||||||
|
PASSWORD=self.options.DB_PASSWORD,
|
||||||
|
)
|
||||||
|
migrate(conf)
|
||||||
|
|
||||||
|
async def init_db(self):
|
||||||
|
conn = await asyncpg.connect(self.connection_str, database="postgres")
|
||||||
|
|
||||||
|
if self.options.DROP_DATABASE_BEFORE_TESTS:
|
||||||
|
await self._drop_template_db(conn)
|
||||||
|
|
||||||
|
await self._create_template_db(conn)
|
||||||
|
await self._create_test_db(conn)
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
if self.options.DROP_DATABASE_BEFORE_TESTS:
|
||||||
|
self._migrate_template_database()
|
||||||
|
|
||||||
|
async def tear_down_db(self):
|
||||||
|
conn = await asyncpg.connect(self.connection_str, database="postgres")
|
||||||
|
|
||||||
|
await self._drop_test_db(conn)
|
||||||
|
|
||||||
|
if self.options.DROP_DATABASE_AFTER_TESTS:
|
||||||
|
await self._drop_template_db(conn)
|
||||||
|
|
||||||
|
await conn.close()
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from app.lib import settings
|
from app.lib import settings
|
||||||
|
|
||||||
@ -14,13 +15,18 @@ class DatabaseConfig:
|
|||||||
|
|
||||||
|
|
||||||
def migrate(conf: DatabaseConfig) -> None:
|
def migrate(conf: DatabaseConfig) -> None:
|
||||||
|
script_path = Path(__file__).parent
|
||||||
|
migrations_path = script_path / "migrations"
|
||||||
|
conf_file = migrations_path / "flyway.conf"
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
settings.db.FLYWAY_PATH,
|
settings.db.FLYWAY_PATH,
|
||||||
"migrate",
|
"migrate",
|
||||||
f"-url=jdbc:postgresql://{conf.HOST}:{conf.PORT}/{conf.NAME}",
|
f"-url=jdbc:postgresql://{conf.HOST}:{conf.PORT}/{conf.NAME}",
|
||||||
f"-user={conf.USER}",
|
f"-user={conf.USER}",
|
||||||
f"-password={conf.PASSWORD}",
|
f"-password={conf.PASSWORD}",
|
||||||
"-configFiles=migrations/flyway.conf",
|
f"-configFiles={conf_file}",
|
||||||
|
f"-locations=filesystem:{migrations_path}",
|
||||||
]
|
]
|
||||||
|
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
|
|||||||
38
poetry.lock
generated
38
poetry.lock
generated
@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
@ -171,6 +171,7 @@ files = [
|
|||||||
{file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
|
{file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
|
||||||
{file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
|
{file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
|
||||||
{file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
|
{file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
|
||||||
|
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"},
|
||||||
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
|
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
|
||||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
|
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
|
||||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
|
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
|
||||||
@ -179,6 +180,7 @@ files = [
|
|||||||
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
|
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
|
||||||
{file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"},
|
{file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"},
|
||||||
{file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
|
{file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
|
||||||
|
{file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"},
|
||||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"},
|
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"},
|
||||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"},
|
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"},
|
||||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
|
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
|
||||||
@ -208,6 +210,7 @@ files = [
|
|||||||
{file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
|
{file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
|
||||||
{file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
|
{file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
|
{file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
|
||||||
|
{file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"},
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
|
{file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
|
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
|
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
|
||||||
@ -216,6 +219,7 @@ files = [
|
|||||||
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
|
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
|
{file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
|
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
|
||||||
|
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"},
|
||||||
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
|
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
|
||||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
|
{file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
|
||||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
|
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
|
||||||
@ -699,6 +703,24 @@ pluggy = ">=0.12,<2.0"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "0.21.1"
|
||||||
|
description = "Pytest support for asyncio"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
|
||||||
|
{file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=7.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
|
||||||
|
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
@ -739,6 +761,7 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||||
|
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||||
@ -746,8 +769,15 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||||
|
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||||
@ -764,6 +794,7 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||||
|
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||||
@ -771,6 +802,7 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||||
|
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||||
@ -849,7 +881,7 @@ files = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
|
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""}
|
||||||
typing-extensions = ">=4.2.0"
|
typing-extensions = ">=4.2.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
@ -942,4 +974,4 @@ anyio = ">=3.0.0"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "28c7f9f4b78e7d602b076108d3138c854cd1a1bc22c447987a6aedbba6300dcd"
|
content-hash = "2f47c374edfb8ee537dd4eeb7406bd443bdf0ed3bdd760e764a363ffd1b4650f"
|
||||||
|
|||||||
@ -18,6 +18,7 @@ pydantic-settings = "^2.0.3"
|
|||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^7.4.2"
|
pytest = "^7.4.2"
|
||||||
watchfiles = "^0.20.0"
|
watchfiles = "^0.20.0"
|
||||||
|
pytest-asyncio = "^0.21.1"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|||||||
11
settings_test.py
Normal file
11
settings_test.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
POSTGRES_HOST: str = "localhost"
|
||||||
|
POSTGRES_PORT: int = 5432
|
||||||
|
POSTGRES_TEST_USER = "addressbook"
|
||||||
|
POSTGRES_TEST_PASSWORD = "addressbook"
|
||||||
|
|
||||||
|
LOG_PATH = "/tmp/addressbook-log"
|
||||||
|
MIN_LOG_LEVEL: int = logging.INFO
|
||||||
|
DROP_TEMPLATE_DATABASE_BEFORE_TESTS: bool = False
|
||||||
|
FLYWAY_BINARY_PATH: str = "/usr/bin/flyway"
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
109
tests/conftest.py
Normal file
109
tests/conftest.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
import pytest_asyncio
|
||||||
|
import sqlalchemy
|
||||||
|
from _pytest.config import Config
|
||||||
|
from sqlalchemy import NullPool, event
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
|
from app.lib import settings
|
||||||
|
from app.lib.sqlalchemy_plugin import _default
|
||||||
|
from app.lib.test_extras.db_setup import TestingDatabaseSetup
|
||||||
|
|
||||||
|
# A Guide To Database Unit Testing with Pytest and SQLAlchemy
|
||||||
|
# https://coderpad.io/blog/development/a-guide-to-database-unit-testing-with-pytest-and-sqlalchemy/
|
||||||
|
|
||||||
|
|
||||||
|
db_connection_url = sqlalchemy.engine.URL.create(
|
||||||
|
drivername="postgresql+asyncpg",
|
||||||
|
username=settings.testing.DB_USER,
|
||||||
|
password=settings.testing.DB_PASSWORD,
|
||||||
|
host=settings.testing.DB_HOST,
|
||||||
|
port=settings.testing.DB_PORT,
|
||||||
|
database=settings.testing.DB_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
engine = create_async_engine(
|
||||||
|
db_connection_url,
|
||||||
|
echo=settings.db.ECHO,
|
||||||
|
echo_pool=settings.db.ECHO_POOL,
|
||||||
|
json_serializer=msgspec.json.Encoder(enc_hook=_default),
|
||||||
|
max_overflow=settings.db.POOL_MAX_OVERFLOW,
|
||||||
|
pool_size=settings.db.POOL_SIZE,
|
||||||
|
pool_timeout=settings.db.POOL_TIMEOUT,
|
||||||
|
poolclass=NullPool if settings.db.POOL_DISABLE else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
async_session_factory = async_sessionmaker(
|
||||||
|
engine, expire_on_commit=False, class_=AsyncSession
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TestingAsyncSessionLocal = async_sessionmaker(
|
||||||
|
engine,
|
||||||
|
expire_on_commit=False,
|
||||||
|
autoflush=False,
|
||||||
|
autocommit=False,
|
||||||
|
class_=AsyncSession,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="function")
|
||||||
|
async def db_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
"""The expectation with async_sessions is that the
|
||||||
|
transactions be called on the connection object instead of the
|
||||||
|
session object.
|
||||||
|
Detailed explanation of async transactional tests
|
||||||
|
<https://github.com/sqlalchemy/sqlalchemy/issues/5811>
|
||||||
|
"""
|
||||||
|
|
||||||
|
async with engine.connect() as connection:
|
||||||
|
trans = await connection.begin()
|
||||||
|
async with TestingAsyncSessionLocal(bind=connection) as async_session:
|
||||||
|
nested = await connection.begin_nested()
|
||||||
|
|
||||||
|
@event.listens_for(async_session.sync_session, "after_transaction_end")
|
||||||
|
def end_savepoint(session, transaction):
|
||||||
|
nonlocal nested
|
||||||
|
|
||||||
|
if not nested.is_active:
|
||||||
|
nested = connection.sync_connection.begin_nested()
|
||||||
|
|
||||||
|
yield async_session
|
||||||
|
|
||||||
|
await trans.rollback()
|
||||||
|
|
||||||
|
await engine.dispose(close=True)
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.fixture(scope="session")
|
||||||
|
# def event_loop():
|
||||||
|
# """
|
||||||
|
# Creates an instance of the default event loop for the test session.
|
||||||
|
# """
|
||||||
|
# policy = asyncio.get_event_loop_policy()
|
||||||
|
# loop = policy.new_event_loop()
|
||||||
|
# yield loop
|
||||||
|
# loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
pytest_plugins = (
|
||||||
|
# "app.lib.test_extras.db_plugins",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure(config: Config) -> None:
|
||||||
|
logging.info(f"Starting tests: {datetime.utcnow()}")
|
||||||
|
db_setup = TestingDatabaseSetup(options=settings.testing)
|
||||||
|
asyncio.run(db_setup.init_db())
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_unconfigure(config: Config) -> None:
|
||||||
|
logging.info(f"Ending tests: {datetime.utcnow()}")
|
||||||
|
db_setup = TestingDatabaseSetup(options=settings.testing)
|
||||||
|
asyncio.run(db_setup.tear_down_db())
|
||||||
41
tests/test_general.py
Normal file
41
tests/test_general.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# A Guide To Database Unit Testing with Pytest and SQLAlchemy
|
||||||
|
# https://coderpad.io/blog/development/a-guide-to-database-unit-testing-with-pytest-and-sqlalchemy/
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import text, event
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||||
|
|
||||||
|
from app.domain.city import City
|
||||||
|
from app.lib import settings
|
||||||
|
from app.lib.sqlalchemy_plugin import engine
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
import sqlalchemy
|
||||||
|
from sqlalchemy.ext.asyncio import (
|
||||||
|
AsyncSession,
|
||||||
|
create_async_engine,
|
||||||
|
async_scoped_session,
|
||||||
|
AsyncConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGeneral:
|
||||||
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
|
def setup_class(self, db_session):
|
||||||
|
self.db_session = db_session
|
||||||
|
|
||||||
|
# async def teardown_class(self):
|
||||||
|
# await self.session.rollback()
|
||||||
|
# await self.session.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bla(self):
|
||||||
|
stmt = text("select * from cities")
|
||||||
|
result = await self.db_session.execute(stmt)
|
||||||
|
print("#"*100)
|
||||||
|
for c in result:
|
||||||
|
print(c)
|
||||||
|
print("#"*100)
|
||||||
|
|
||||||
|
assert True
|
||||||
Reference in New Issue
Block a user