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.
Files changed (71) hide show
  1. fixturify/__init__.py +21 -0
  2. fixturify/_utils/__init__.py +7 -0
  3. fixturify/_utils/_constants.py +10 -0
  4. fixturify/_utils/_fixture_discovery.py +165 -0
  5. fixturify/_utils/_path_resolver.py +135 -0
  6. fixturify/http_d/__init__.py +80 -0
  7. fixturify/http_d/_config.py +214 -0
  8. fixturify/http_d/_decorator.py +267 -0
  9. fixturify/http_d/_exceptions.py +153 -0
  10. fixturify/http_d/_fixture_discovery.py +33 -0
  11. fixturify/http_d/_matcher.py +372 -0
  12. fixturify/http_d/_mock_context.py +154 -0
  13. fixturify/http_d/_models.py +205 -0
  14. fixturify/http_d/_patcher.py +524 -0
  15. fixturify/http_d/_player.py +222 -0
  16. fixturify/http_d/_recorder.py +1350 -0
  17. fixturify/http_d/_stubs/__init__.py +8 -0
  18. fixturify/http_d/_stubs/_aiohttp.py +220 -0
  19. fixturify/http_d/_stubs/_connection.py +478 -0
  20. fixturify/http_d/_stubs/_httpcore.py +269 -0
  21. fixturify/http_d/_stubs/_tornado.py +95 -0
  22. fixturify/http_d/_utils.py +194 -0
  23. fixturify/json_assert/__init__.py +13 -0
  24. fixturify/json_assert/_actual_saver.py +67 -0
  25. fixturify/json_assert/_assert.py +173 -0
  26. fixturify/json_assert/_comparator.py +183 -0
  27. fixturify/json_assert/_diff_formatter.py +265 -0
  28. fixturify/json_assert/_normalizer.py +83 -0
  29. fixturify/object_mapper/__init__.py +5 -0
  30. fixturify/object_mapper/_deserializers/__init__.py +19 -0
  31. fixturify/object_mapper/_deserializers/_base.py +186 -0
  32. fixturify/object_mapper/_deserializers/_dataclass.py +52 -0
  33. fixturify/object_mapper/_deserializers/_plain.py +55 -0
  34. fixturify/object_mapper/_deserializers/_pydantic_v1.py +38 -0
  35. fixturify/object_mapper/_deserializers/_pydantic_v2.py +41 -0
  36. fixturify/object_mapper/_deserializers/_sqlalchemy.py +72 -0
  37. fixturify/object_mapper/_deserializers/_sqlmodel.py +43 -0
  38. fixturify/object_mapper/_detectors/__init__.py +5 -0
  39. fixturify/object_mapper/_detectors/_type_detector.py +186 -0
  40. fixturify/object_mapper/_serializers/__init__.py +19 -0
  41. fixturify/object_mapper/_serializers/_base.py +260 -0
  42. fixturify/object_mapper/_serializers/_dataclass.py +55 -0
  43. fixturify/object_mapper/_serializers/_plain.py +49 -0
  44. fixturify/object_mapper/_serializers/_pydantic_v1.py +49 -0
  45. fixturify/object_mapper/_serializers/_pydantic_v2.py +49 -0
  46. fixturify/object_mapper/_serializers/_sqlalchemy.py +70 -0
  47. fixturify/object_mapper/_serializers/_sqlmodel.py +54 -0
  48. fixturify/object_mapper/mapper.py +256 -0
  49. fixturify/read_d/__init__.py +5 -0
  50. fixturify/read_d/_decorator.py +193 -0
  51. fixturify/read_d/_fixture_loader.py +88 -0
  52. fixturify/sql_d/__init__.py +7 -0
  53. fixturify/sql_d/_config.py +30 -0
  54. fixturify/sql_d/_decorator.py +373 -0
  55. fixturify/sql_d/_driver_registry.py +133 -0
  56. fixturify/sql_d/_executor.py +82 -0
  57. fixturify/sql_d/_fixture_discovery.py +55 -0
  58. fixturify/sql_d/_phase.py +10 -0
  59. fixturify/sql_d/_strategies/__init__.py +11 -0
  60. fixturify/sql_d/_strategies/_aiomysql.py +63 -0
  61. fixturify/sql_d/_strategies/_aiosqlite.py +29 -0
  62. fixturify/sql_d/_strategies/_asyncpg.py +34 -0
  63. fixturify/sql_d/_strategies/_base.py +118 -0
  64. fixturify/sql_d/_strategies/_mysql.py +70 -0
  65. fixturify/sql_d/_strategies/_psycopg.py +35 -0
  66. fixturify/sql_d/_strategies/_psycopg2.py +40 -0
  67. fixturify/sql_d/_strategies/_registry.py +109 -0
  68. fixturify/sql_d/_strategies/_sqlite.py +33 -0
  69. fixturify-0.1.9.dist-info/METADATA +122 -0
  70. fixturify-0.1.9.dist-info/RECORD +71 -0
  71. 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
+ ```