fixturify 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fixturify/__init__.py +21 -0
- fixturify/_utils/__init__.py +7 -0
- fixturify/_utils/_constants.py +10 -0
- fixturify/_utils/_fixture_discovery.py +165 -0
- fixturify/_utils/_path_resolver.py +135 -0
- fixturify/http_d/__init__.py +80 -0
- fixturify/http_d/_config.py +214 -0
- fixturify/http_d/_decorator.py +267 -0
- fixturify/http_d/_exceptions.py +153 -0
- fixturify/http_d/_fixture_discovery.py +33 -0
- fixturify/http_d/_matcher.py +372 -0
- fixturify/http_d/_mock_context.py +154 -0
- fixturify/http_d/_models.py +205 -0
- fixturify/http_d/_patcher.py +524 -0
- fixturify/http_d/_player.py +222 -0
- fixturify/http_d/_recorder.py +1350 -0
- fixturify/http_d/_stubs/__init__.py +8 -0
- fixturify/http_d/_stubs/_aiohttp.py +220 -0
- fixturify/http_d/_stubs/_connection.py +478 -0
- fixturify/http_d/_stubs/_httpcore.py +269 -0
- fixturify/http_d/_stubs/_tornado.py +95 -0
- fixturify/http_d/_utils.py +194 -0
- fixturify/json_assert/__init__.py +13 -0
- fixturify/json_assert/_actual_saver.py +67 -0
- fixturify/json_assert/_assert.py +173 -0
- fixturify/json_assert/_comparator.py +183 -0
- fixturify/json_assert/_diff_formatter.py +265 -0
- fixturify/json_assert/_normalizer.py +83 -0
- fixturify/object_mapper/__init__.py +5 -0
- fixturify/object_mapper/_deserializers/__init__.py +19 -0
- fixturify/object_mapper/_deserializers/_base.py +186 -0
- fixturify/object_mapper/_deserializers/_dataclass.py +52 -0
- fixturify/object_mapper/_deserializers/_plain.py +55 -0
- fixturify/object_mapper/_deserializers/_pydantic_v1.py +38 -0
- fixturify/object_mapper/_deserializers/_pydantic_v2.py +41 -0
- fixturify/object_mapper/_deserializers/_sqlalchemy.py +72 -0
- fixturify/object_mapper/_deserializers/_sqlmodel.py +43 -0
- fixturify/object_mapper/_detectors/__init__.py +5 -0
- fixturify/object_mapper/_detectors/_type_detector.py +186 -0
- fixturify/object_mapper/_serializers/__init__.py +19 -0
- fixturify/object_mapper/_serializers/_base.py +260 -0
- fixturify/object_mapper/_serializers/_dataclass.py +55 -0
- fixturify/object_mapper/_serializers/_plain.py +49 -0
- fixturify/object_mapper/_serializers/_pydantic_v1.py +49 -0
- fixturify/object_mapper/_serializers/_pydantic_v2.py +49 -0
- fixturify/object_mapper/_serializers/_sqlalchemy.py +70 -0
- fixturify/object_mapper/_serializers/_sqlmodel.py +54 -0
- fixturify/object_mapper/mapper.py +256 -0
- fixturify/read_d/__init__.py +5 -0
- fixturify/read_d/_decorator.py +193 -0
- fixturify/read_d/_fixture_loader.py +88 -0
- fixturify/sql_d/__init__.py +7 -0
- fixturify/sql_d/_config.py +30 -0
- fixturify/sql_d/_decorator.py +373 -0
- fixturify/sql_d/_driver_registry.py +133 -0
- fixturify/sql_d/_executor.py +82 -0
- fixturify/sql_d/_fixture_discovery.py +55 -0
- fixturify/sql_d/_phase.py +10 -0
- fixturify/sql_d/_strategies/__init__.py +11 -0
- fixturify/sql_d/_strategies/_aiomysql.py +63 -0
- fixturify/sql_d/_strategies/_aiosqlite.py +29 -0
- fixturify/sql_d/_strategies/_asyncpg.py +34 -0
- fixturify/sql_d/_strategies/_base.py +118 -0
- fixturify/sql_d/_strategies/_mysql.py +70 -0
- fixturify/sql_d/_strategies/_psycopg.py +35 -0
- fixturify/sql_d/_strategies/_psycopg2.py +40 -0
- fixturify/sql_d/_strategies/_registry.py +109 -0
- fixturify/sql_d/_strategies/_sqlite.py +33 -0
- fixturify-0.1.9.dist-info/METADATA +122 -0
- fixturify-0.1.9.dist-info/RECORD +71 -0
- fixturify-0.1.9.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Strategy for aiomysql (MySQL async) driver."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ._base import AsyncSqlExecutionStrategy
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .._config import SqlTestConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AiomysqlStrategy(AsyncSqlExecutionStrategy):
|
|
12
|
+
"""Execution strategy for aiomysql (MySQL async)."""
|
|
13
|
+
|
|
14
|
+
driver_name = "aiomysql"
|
|
15
|
+
is_async = True
|
|
16
|
+
default_port = 3306
|
|
17
|
+
param_mapping = {
|
|
18
|
+
"host": "host",
|
|
19
|
+
"database": "db",
|
|
20
|
+
"user": "user",
|
|
21
|
+
"password": "password",
|
|
22
|
+
"port": "port",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async def execute_async(self, sql_content: str, config: "SqlTestConfig") -> None:
|
|
26
|
+
"""Execute SQL using aiomysql."""
|
|
27
|
+
driver = self.get_driver_module()
|
|
28
|
+
params = self.build_connection_params(config)
|
|
29
|
+
|
|
30
|
+
connection = await driver.connect(**params)
|
|
31
|
+
try:
|
|
32
|
+
async with connection.cursor() as cursor:
|
|
33
|
+
# aiomysql doesn't support multi-statement by default
|
|
34
|
+
await self._execute_statements(cursor, sql_content)
|
|
35
|
+
await connection.commit()
|
|
36
|
+
finally:
|
|
37
|
+
connection.close()
|
|
38
|
+
if hasattr(connection, "wait_closed"):
|
|
39
|
+
await connection.wait_closed()
|
|
40
|
+
|
|
41
|
+
async def _execute_statements(self, cursor: Any, sql_content: str) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Execute multiple SQL statements for aiomysql.
|
|
44
|
+
|
|
45
|
+
aiomysql doesn't execute multiple statements by default.
|
|
46
|
+
We split by semicolon and execute each statement separately.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
cursor: Async database cursor
|
|
50
|
+
sql_content: SQL content with potentially multiple statements
|
|
51
|
+
"""
|
|
52
|
+
statements = [s.strip() for s in sql_content.split(";") if s.strip()]
|
|
53
|
+
for statement in statements:
|
|
54
|
+
# Skip comments-only statements
|
|
55
|
+
if statement.startswith("--") and "\n" not in statement:
|
|
56
|
+
continue
|
|
57
|
+
await cursor.execute(statement)
|
|
58
|
+
# Consume any results to avoid unread result issues
|
|
59
|
+
try:
|
|
60
|
+
await cursor.fetchall()
|
|
61
|
+
except Exception:
|
|
62
|
+
# No results to fetch (e.g., INSERT, UPDATE, DELETE, DDL)
|
|
63
|
+
pass
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Strategy for aiosqlite (SQLite async) driver."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ._base import AsyncSqlExecutionStrategy
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .._config import SqlTestConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AiosqliteStrategy(AsyncSqlExecutionStrategy):
|
|
12
|
+
"""Execution strategy for aiosqlite (SQLite async)."""
|
|
13
|
+
|
|
14
|
+
driver_name = "aiosqlite"
|
|
15
|
+
is_async = True
|
|
16
|
+
default_port = 0 # SQLite doesn't use ports
|
|
17
|
+
param_mapping = {
|
|
18
|
+
"database": "database",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async def execute_async(self, sql_content: str, config: "SqlTestConfig") -> None:
|
|
22
|
+
"""Execute SQL using aiosqlite."""
|
|
23
|
+
driver = self.get_driver_module()
|
|
24
|
+
params = self.build_connection_params(config)
|
|
25
|
+
|
|
26
|
+
database_path = params.get("database", ":memory:")
|
|
27
|
+
async with driver.connect(database_path) as connection:
|
|
28
|
+
await connection.executescript(sql_content)
|
|
29
|
+
await connection.commit()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Strategy for asyncpg (PostgreSQL async) driver."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ._base import AsyncSqlExecutionStrategy
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .._config import SqlTestConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncpgStrategy(AsyncSqlExecutionStrategy):
|
|
12
|
+
"""Execution strategy for asyncpg (PostgreSQL async)."""
|
|
13
|
+
|
|
14
|
+
driver_name = "asyncpg"
|
|
15
|
+
is_async = True
|
|
16
|
+
default_port = 5432
|
|
17
|
+
param_mapping = {
|
|
18
|
+
"host": "host",
|
|
19
|
+
"database": "database",
|
|
20
|
+
"user": "user",
|
|
21
|
+
"password": "password",
|
|
22
|
+
"port": "port",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async def execute_async(self, sql_content: str, config: "SqlTestConfig") -> None:
|
|
26
|
+
"""Execute SQL using asyncpg."""
|
|
27
|
+
driver = self.get_driver_module()
|
|
28
|
+
params = self.build_connection_params(config)
|
|
29
|
+
|
|
30
|
+
connection = await driver.connect(**params)
|
|
31
|
+
try:
|
|
32
|
+
await connection.execute(sql_content)
|
|
33
|
+
finally:
|
|
34
|
+
await connection.close()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Base classes for SQL execution strategies."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, Dict, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .._config import SqlTestConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SqlExecutionStrategy(ABC):
|
|
11
|
+
"""
|
|
12
|
+
Abstract base class for synchronous SQL execution strategies.
|
|
13
|
+
|
|
14
|
+
Each strategy handles execution for a specific database driver.
|
|
15
|
+
Strategies encapsulate driver-specific behavior like:
|
|
16
|
+
- Connection handling
|
|
17
|
+
- Multi-statement execution
|
|
18
|
+
- Error handling
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# Driver name (used for registration)
|
|
22
|
+
driver_name: str = ""
|
|
23
|
+
|
|
24
|
+
# Whether this is an async strategy
|
|
25
|
+
is_async: bool = False
|
|
26
|
+
|
|
27
|
+
# Default port for this driver
|
|
28
|
+
default_port: int = 0
|
|
29
|
+
|
|
30
|
+
# Parameter name mapping from SqlTestConfig to driver-specific names
|
|
31
|
+
param_mapping: Dict[str, str] = {}
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def execute(self, sql_content: str, config: "SqlTestConfig") -> None:
|
|
35
|
+
"""
|
|
36
|
+
Execute SQL content against the database.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
sql_content: SQL statements to execute
|
|
40
|
+
config: Database connection configuration
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def build_connection_params(self, config: "SqlTestConfig") -> Dict[str, Any]:
|
|
45
|
+
"""
|
|
46
|
+
Build connection parameters for this driver.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
config: Database connection configuration
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dictionary of driver-specific connection parameters
|
|
53
|
+
"""
|
|
54
|
+
params: Dict[str, Any] = {}
|
|
55
|
+
|
|
56
|
+
if "host" in self.param_mapping:
|
|
57
|
+
params[self.param_mapping["host"]] = config.host
|
|
58
|
+
|
|
59
|
+
if "database" in self.param_mapping:
|
|
60
|
+
params[self.param_mapping["database"]] = config.database
|
|
61
|
+
|
|
62
|
+
if "user" in self.param_mapping:
|
|
63
|
+
params[self.param_mapping["user"]] = config.user
|
|
64
|
+
|
|
65
|
+
if "password" in self.param_mapping:
|
|
66
|
+
params[self.param_mapping["password"]] = config.password
|
|
67
|
+
|
|
68
|
+
if "port" in self.param_mapping:
|
|
69
|
+
port = config.port or self.default_port
|
|
70
|
+
if port: # Skip if port is 0 (e.g., SQLite)
|
|
71
|
+
params[self.param_mapping["port"]] = port
|
|
72
|
+
|
|
73
|
+
return params
|
|
74
|
+
|
|
75
|
+
def get_driver_module(self) -> Any:
|
|
76
|
+
"""
|
|
77
|
+
Import and return the driver module.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The imported driver module
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ImportError: If driver is not installed
|
|
84
|
+
"""
|
|
85
|
+
import importlib
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
return importlib.import_module(self.driver_name)
|
|
89
|
+
except ImportError as e:
|
|
90
|
+
raise ImportError(
|
|
91
|
+
f"Database driver '{self.driver_name}' is not installed. "
|
|
92
|
+
f"Install it with: pip install {self.driver_name}"
|
|
93
|
+
) from e
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class AsyncSqlExecutionStrategy(SqlExecutionStrategy):
|
|
97
|
+
"""
|
|
98
|
+
Abstract base class for asynchronous SQL execution strategies.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
is_async: bool = True
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
async def execute_async(self, sql_content: str, config: "SqlTestConfig") -> None:
|
|
105
|
+
"""
|
|
106
|
+
Execute SQL content against the database asynchronously.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
sql_content: SQL statements to execute
|
|
110
|
+
config: Database connection configuration
|
|
111
|
+
"""
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def execute(self, sql_content: str, config: "SqlTestConfig") -> None:
|
|
115
|
+
"""Sync execute - raises error for async strategies."""
|
|
116
|
+
raise RuntimeError(
|
|
117
|
+
f"Strategy {self.driver_name} is async. Use execute_async() instead."
|
|
118
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Strategy for mysql.connector (MySQL) driver."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ._base import SqlExecutionStrategy
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .._config import SqlTestConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MysqlConnectorStrategy(SqlExecutionStrategy):
|
|
12
|
+
"""Execution strategy for mysql.connector (MySQL)."""
|
|
13
|
+
|
|
14
|
+
driver_name = "mysql.connector"
|
|
15
|
+
is_async = False
|
|
16
|
+
default_port = 3306
|
|
17
|
+
param_mapping = {
|
|
18
|
+
"host": "host",
|
|
19
|
+
"database": "database",
|
|
20
|
+
"user": "user",
|
|
21
|
+
"password": "password",
|
|
22
|
+
"port": "port",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def execute(self, sql_content: str, config: "SqlTestConfig") -> None:
|
|
26
|
+
"""Execute SQL using mysql.connector."""
|
|
27
|
+
driver = self.get_driver_module()
|
|
28
|
+
params = self.build_connection_params(config)
|
|
29
|
+
|
|
30
|
+
connection = driver.connect(**params)
|
|
31
|
+
try:
|
|
32
|
+
cursor = connection.cursor()
|
|
33
|
+
try:
|
|
34
|
+
# MySQL connector doesn't support multi-statement by default
|
|
35
|
+
self._execute_statements(cursor, sql_content, driver)
|
|
36
|
+
connection.commit()
|
|
37
|
+
finally:
|
|
38
|
+
cursor.close()
|
|
39
|
+
finally:
|
|
40
|
+
connection.close()
|
|
41
|
+
|
|
42
|
+
def _execute_statements(self, cursor: Any, sql_content: str, driver: Any) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Execute multiple SQL statements for MySQL.
|
|
45
|
+
|
|
46
|
+
MySQL connector doesn't execute multiple statements by default.
|
|
47
|
+
We split by semicolon and execute each statement separately.
|
|
48
|
+
|
|
49
|
+
**Limitations:**
|
|
50
|
+
- This naive splitting breaks with semicolons inside string literals
|
|
51
|
+
- Stored procedures with DELIMITER changes are not supported
|
|
52
|
+
- Multi-line comments containing semicolons may cause issues
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
cursor: Database cursor
|
|
56
|
+
sql_content: SQL content with potentially multiple statements
|
|
57
|
+
driver: The mysql.connector module
|
|
58
|
+
"""
|
|
59
|
+
statements = [s.strip() for s in sql_content.split(";") if s.strip()]
|
|
60
|
+
for statement in statements:
|
|
61
|
+
# Skip comments-only statements
|
|
62
|
+
if statement.startswith("--") and "\n" not in statement:
|
|
63
|
+
continue
|
|
64
|
+
cursor.execute(statement)
|
|
65
|
+
# Consume any results to avoid "Unread result found" error
|
|
66
|
+
try:
|
|
67
|
+
cursor.fetchall()
|
|
68
|
+
except driver.errors.InterfaceError:
|
|
69
|
+
# No results to fetch (e.g., INSERT, UPDATE, DELETE, DDL)
|
|
70
|
+
pass
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Strategy for psycopg (PostgreSQL async) driver."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ._base import AsyncSqlExecutionStrategy
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .._config import SqlTestConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PsycopgStrategy(AsyncSqlExecutionStrategy):
|
|
12
|
+
"""Execution strategy for psycopg (PostgreSQL async)."""
|
|
13
|
+
|
|
14
|
+
driver_name = "psycopg"
|
|
15
|
+
is_async = True
|
|
16
|
+
default_port = 5432
|
|
17
|
+
param_mapping = {
|
|
18
|
+
"host": "host",
|
|
19
|
+
"database": "dbname",
|
|
20
|
+
"user": "user",
|
|
21
|
+
"password": "password",
|
|
22
|
+
"port": "port",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async def execute_async(self, sql_content: str, config: "SqlTestConfig") -> None:
|
|
26
|
+
"""Execute SQL using psycopg async connection."""
|
|
27
|
+
driver = self.get_driver_module()
|
|
28
|
+
params = self.build_connection_params(config)
|
|
29
|
+
|
|
30
|
+
connection = await driver.AsyncConnection.connect(**params)
|
|
31
|
+
try:
|
|
32
|
+
await connection.execute(sql_content)
|
|
33
|
+
await connection.commit()
|
|
34
|
+
finally:
|
|
35
|
+
await connection.close()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Strategy for psycopg2 (PostgreSQL) driver."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ._base import SqlExecutionStrategy
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .._config import SqlTestConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Psycopg2Strategy(SqlExecutionStrategy):
|
|
12
|
+
"""Execution strategy for psycopg2 (PostgreSQL)."""
|
|
13
|
+
|
|
14
|
+
driver_name = "psycopg2"
|
|
15
|
+
is_async = False
|
|
16
|
+
default_port = 5432
|
|
17
|
+
param_mapping = {
|
|
18
|
+
"host": "host",
|
|
19
|
+
"database": "dbname",
|
|
20
|
+
"user": "user",
|
|
21
|
+
"password": "password",
|
|
22
|
+
"port": "port",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def execute(self, sql_content: str, config: "SqlTestConfig") -> None:
|
|
26
|
+
"""Execute SQL using psycopg2."""
|
|
27
|
+
driver = self.get_driver_module()
|
|
28
|
+
params = self.build_connection_params(config)
|
|
29
|
+
|
|
30
|
+
connection = driver.connect(**params)
|
|
31
|
+
try:
|
|
32
|
+
cursor = connection.cursor()
|
|
33
|
+
try:
|
|
34
|
+
# psycopg2 supports multi-statement execution
|
|
35
|
+
cursor.execute(sql_content)
|
|
36
|
+
connection.commit()
|
|
37
|
+
finally:
|
|
38
|
+
cursor.close()
|
|
39
|
+
finally:
|
|
40
|
+
connection.close()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Registry for SQL execution strategies."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional, Type
|
|
4
|
+
|
|
5
|
+
from ._base import SqlExecutionStrategy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StrategyRegistry:
|
|
9
|
+
"""
|
|
10
|
+
Registry for SQL execution strategies.
|
|
11
|
+
|
|
12
|
+
Maps driver names to strategy classes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
"""Initialize empty registry."""
|
|
17
|
+
self._strategies: Dict[str, SqlExecutionStrategy] = {}
|
|
18
|
+
self._strategy_classes: Dict[str, Type[SqlExecutionStrategy]] = {}
|
|
19
|
+
|
|
20
|
+
def register(self, strategy_class: Type[SqlExecutionStrategy]) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Register a strategy class.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
strategy_class: The strategy class to register.
|
|
26
|
+
"""
|
|
27
|
+
name = strategy_class.driver_name
|
|
28
|
+
if not name:
|
|
29
|
+
raise ValueError(f"Strategy class {strategy_class} has no driver_name")
|
|
30
|
+
self._strategy_classes[name] = strategy_class
|
|
31
|
+
|
|
32
|
+
def get(self, driver_name: str) -> Optional[SqlExecutionStrategy]:
|
|
33
|
+
"""
|
|
34
|
+
Get a strategy instance by driver name.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
driver_name: The driver name.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Strategy instance or None if not registered.
|
|
41
|
+
"""
|
|
42
|
+
if driver_name not in self._strategies:
|
|
43
|
+
if driver_name in self._strategy_classes:
|
|
44
|
+
self._strategies[driver_name] = self._strategy_classes[driver_name]()
|
|
45
|
+
return self._strategies.get(driver_name)
|
|
46
|
+
|
|
47
|
+
def get_or_raise(self, driver_name: str) -> SqlExecutionStrategy:
|
|
48
|
+
"""
|
|
49
|
+
Get a strategy instance, raising if not found.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
driver_name: The driver name.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Strategy instance.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: If driver is not registered.
|
|
59
|
+
"""
|
|
60
|
+
strategy = self.get(driver_name)
|
|
61
|
+
if strategy is None:
|
|
62
|
+
available = ", ".join(self._strategy_classes.keys())
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Unsupported driver: {driver_name}. "
|
|
65
|
+
f"Available drivers: {available}"
|
|
66
|
+
)
|
|
67
|
+
return strategy
|
|
68
|
+
|
|
69
|
+
def is_async(self, driver_name: str) -> bool:
|
|
70
|
+
"""Check if a driver uses async execution."""
|
|
71
|
+
strategy = self.get(driver_name)
|
|
72
|
+
return strategy.is_async if strategy else False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Global registry instance
|
|
76
|
+
_registry: Optional[StrategyRegistry] = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_strategy_registry() -> StrategyRegistry:
|
|
80
|
+
"""
|
|
81
|
+
Get the global strategy registry, initializing if needed.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The global StrategyRegistry instance.
|
|
85
|
+
"""
|
|
86
|
+
global _registry
|
|
87
|
+
if _registry is None:
|
|
88
|
+
_registry = StrategyRegistry()
|
|
89
|
+
_register_default_strategies(_registry)
|
|
90
|
+
return _registry
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _register_default_strategies(registry: StrategyRegistry) -> None:
|
|
94
|
+
"""Register all built-in strategies."""
|
|
95
|
+
from ._psycopg2 import Psycopg2Strategy
|
|
96
|
+
from ._psycopg import PsycopgStrategy
|
|
97
|
+
from ._mysql import MysqlConnectorStrategy
|
|
98
|
+
from ._sqlite import Sqlite3Strategy
|
|
99
|
+
from ._asyncpg import AsyncpgStrategy
|
|
100
|
+
from ._aiomysql import AiomysqlStrategy
|
|
101
|
+
from ._aiosqlite import AiosqliteStrategy
|
|
102
|
+
|
|
103
|
+
registry.register(Psycopg2Strategy)
|
|
104
|
+
registry.register(PsycopgStrategy)
|
|
105
|
+
registry.register(MysqlConnectorStrategy)
|
|
106
|
+
registry.register(Sqlite3Strategy)
|
|
107
|
+
registry.register(AsyncpgStrategy)
|
|
108
|
+
registry.register(AiomysqlStrategy)
|
|
109
|
+
registry.register(AiosqliteStrategy)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Strategy for sqlite3 driver."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ._base import SqlExecutionStrategy
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .._config import SqlTestConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Sqlite3Strategy(SqlExecutionStrategy):
|
|
12
|
+
"""Execution strategy for sqlite3."""
|
|
13
|
+
|
|
14
|
+
driver_name = "sqlite3"
|
|
15
|
+
is_async = False
|
|
16
|
+
default_port = 0 # SQLite doesn't use ports
|
|
17
|
+
param_mapping = {
|
|
18
|
+
"database": "database",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def execute(self, sql_content: str, config: "SqlTestConfig") -> None:
|
|
22
|
+
"""Execute SQL using sqlite3."""
|
|
23
|
+
driver = self.get_driver_module()
|
|
24
|
+
params = self.build_connection_params(config)
|
|
25
|
+
|
|
26
|
+
database_path = params.get("database", ":memory:")
|
|
27
|
+
connection = driver.connect(database_path)
|
|
28
|
+
try:
|
|
29
|
+
# SQLite supports executescript for multi-statement execution
|
|
30
|
+
connection.executescript(sql_content)
|
|
31
|
+
connection.commit()
|
|
32
|
+
finally:
|
|
33
|
+
connection.close()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fixturify
|
|
3
|
+
Version: 0.1.9
|
|
4
|
+
Summary: A collection of convenient testing utilities for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/eleven-sea/pytools
|
|
6
|
+
Project-URL: Repository, https://github.com/eleven-sea/pytools
|
|
7
|
+
Project-URL: Issues, https://github.com/eleven-sea/pytools/issues
|
|
8
|
+
Author: eleven-sea
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: fixtures,json,mocking,pytest,sql,testing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: Pytest
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: deepdiff>=6.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# pytools
|
|
25
|
+
|
|
26
|
+
A collection of Python testing utilities.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install fixturify
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Modules
|
|
35
|
+
|
|
36
|
+
| Module | Description | Docs |
|
|
37
|
+
|--------|-------------|------|
|
|
38
|
+
| [sql](docs/sql.md) | Execute SQL files before/after tests | [docs/sql.md](docs/sql.md) |
|
|
39
|
+
| [read](docs/read.md) | Inject JSON fixtures into test functions | [docs/read.md](docs/read.md) |
|
|
40
|
+
| [http](docs/http.md) | Record and replay HTTP calls | [docs/http.md](docs/http.md) |
|
|
41
|
+
| [JsonAssert](docs/json_assert.md) | Compare objects to JSON files | [docs/json_assert.md](docs/json_assert.md) |
|
|
42
|
+
| [ObjectMapper](docs/object_mapper.md) | Bidirectional object-JSON mapping | [docs/object_mapper.md](docs/object_mapper.md) |
|
|
43
|
+
|
|
44
|
+
## Quick examples
|
|
45
|
+
|
|
46
|
+
### sql
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from fixturify import sql, Phase
|
|
50
|
+
|
|
51
|
+
@sql(path="./setup.sql")
|
|
52
|
+
@sql(path="./cleanup.sql", phase=Phase.AFTER)
|
|
53
|
+
def test_database():
|
|
54
|
+
# SQL executed before and after test
|
|
55
|
+
pass
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### read
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from fixturify import read
|
|
62
|
+
|
|
63
|
+
@read.fixture(path="./fixtures/user.json", fixture_name="user", object_class=User)
|
|
64
|
+
def test_user(user: User):
|
|
65
|
+
assert user.name == "John"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### http
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from fixturify import http
|
|
72
|
+
|
|
73
|
+
@http(path="./fixtures/api.json")
|
|
74
|
+
def test_api():
|
|
75
|
+
# First run: records HTTP calls
|
|
76
|
+
# Next runs: replays from file
|
|
77
|
+
response = requests.get("https://api.example.com/users")
|
|
78
|
+
assert response.status_code == 200
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### JsonAssert
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from fixturify import JsonAssert
|
|
85
|
+
|
|
86
|
+
def test_response():
|
|
87
|
+
data = {"name": "John", "age": 30}
|
|
88
|
+
JsonAssert(data).ignore("age").compare_to_file("./expected.json")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### ObjectMapper
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from fixturify import ObjectMapper
|
|
95
|
+
|
|
96
|
+
# Object to JSON
|
|
97
|
+
user = User(name="John", age=30)
|
|
98
|
+
data = ObjectMapper(user).to_json()
|
|
99
|
+
|
|
100
|
+
# JSON to object
|
|
101
|
+
data = {"name": "John", "age": 30}
|
|
102
|
+
user = ObjectMapper(data).to_object(User)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Public API
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from fixturify import (
|
|
109
|
+
# Decorators
|
|
110
|
+
sql,
|
|
111
|
+
read,
|
|
112
|
+
http,
|
|
113
|
+
# Enums
|
|
114
|
+
Phase,
|
|
115
|
+
# Config
|
|
116
|
+
SqlTestConfig,
|
|
117
|
+
HttpTestConfig,
|
|
118
|
+
# Classes
|
|
119
|
+
JsonAssert,
|
|
120
|
+
ObjectMapper,
|
|
121
|
+
)
|
|
122
|
+
```
|