fixturify 0.1.11__py3-none-any.whl → 0.1.12__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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """PyTools - A collection of reusable Python utility modules."""
2
2
 
3
- __version__ = "0.1.11"
3
+ __version__ = "0.1.12"
4
4
 
5
5
  from fixturify.sql_d import sql, Phase, SqlTestConfig
6
6
  from fixturify.read_d import read
@@ -811,10 +811,36 @@ def create_response_from_http_client(
811
811
  Returns:
812
812
  HttpResponse model
813
813
  """
814
+ import gzip
815
+ import zlib
816
+
814
817
  headers = dict(response.getheaders()) if hasattr(response, "getheaders") else {}
815
818
  content_type = headers.get("content-type", headers.get("Content-Type", ""))
819
+ content_encoding = headers.get(
820
+ "content-encoding", headers.get("Content-Encoding", "")
821
+ ).lower()
816
822
 
817
823
  body_bytes = response.read()
824
+
825
+ # Decompress if needed (http.client doesn't auto-decompress)
826
+ if body_bytes and content_encoding in ("gzip", "deflate"):
827
+ try:
828
+ if content_encoding == "gzip":
829
+ body_bytes = gzip.decompress(body_bytes)
830
+ elif content_encoding == "deflate":
831
+ # deflate can be raw or zlib-wrapped
832
+ try:
833
+ body_bytes = zlib.decompress(body_bytes)
834
+ except zlib.error:
835
+ body_bytes = zlib.decompress(body_bytes, -zlib.MAX_WBITS)
836
+ # Remove content-encoding header since we decompressed
837
+ headers = {
838
+ k: v for k, v in headers.items()
839
+ if k.lower() != "content-encoding"
840
+ }
841
+ except (gzip.BadGzipFile, OSError, zlib.error):
842
+ pass # Not actually compressed, use as-is
843
+
818
844
  body, body_encoding, is_binary = _serialize_body(body_bytes, content_type)
819
845
 
820
846
  if is_binary:
@@ -1312,18 +1338,43 @@ def create_response_from_httpcore(
1312
1338
  Returns:
1313
1339
  HttpResponse model
1314
1340
  """
1341
+ import gzip
1342
+ import zlib
1343
+
1315
1344
  # Convert headers from list of tuples to dict
1316
1345
  headers_dict: Dict[str, str] = {}
1346
+ content_encoding = ""
1317
1347
  if response.headers:
1318
1348
  for k, v in response.headers:
1319
1349
  key = k.decode() if isinstance(k, bytes) else k
1320
1350
  val = v.decode() if isinstance(v, bytes) else v
1321
1351
  headers_dict[key] = val
1352
+ if key.lower() == "content-encoding":
1353
+ content_encoding = val.lower()
1322
1354
 
1323
1355
  content_type = headers_dict.get("content-type", headers_dict.get("Content-Type", ""))
1324
1356
 
1325
1357
  # Get body content
1326
1358
  body_bytes = response.content
1359
+
1360
+ # Decompress if needed (httpcore doesn't auto-decompress)
1361
+ if body_bytes and content_encoding in ("gzip", "deflate"):
1362
+ try:
1363
+ if content_encoding == "gzip":
1364
+ body_bytes = gzip.decompress(body_bytes)
1365
+ elif content_encoding == "deflate":
1366
+ try:
1367
+ body_bytes = zlib.decompress(body_bytes)
1368
+ except zlib.error:
1369
+ body_bytes = zlib.decompress(body_bytes, -zlib.MAX_WBITS)
1370
+ # Remove content-encoding header since we decompressed
1371
+ headers_dict = {
1372
+ k: v for k, v in headers_dict.items()
1373
+ if k.lower() != "content-encoding"
1374
+ }
1375
+ except (gzip.BadGzipFile, OSError, zlib.error):
1376
+ pass # Not actually compressed, use as-is
1377
+
1327
1378
  body, body_encoding, is_binary = _serialize_body(body_bytes, content_type)
1328
1379
 
1329
1380
  if is_binary:
@@ -9,7 +9,7 @@ import functools
9
9
  from typing import TYPE_CHECKING, Any
10
10
 
11
11
  from .._models import HttpRequest, HttpResponse
12
- from .._recorder import create_request_from_httpcore, create_response_from_httpcore
12
+ from .._recorder import create_request_from_httpcore
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from .._mock_context import HttpMockContext
@@ -72,13 +72,14 @@ def _build_response(response_model: HttpResponse) -> Any:
72
72
  """Build httpcore Response from HttpResponse model."""
73
73
  from httpcore import Response
74
74
  import gzip
75
-
75
+ import zlib
76
+
76
77
  # Get content encoding before filtering headers
77
78
  content_encoding = response_model.headers.get(
78
- "content-encoding",
79
+ "content-encoding",
79
80
  response_model.headers.get("Content-Encoding", "")
80
81
  ).lower()
81
-
82
+
82
83
  # Build headers as list of tuples
83
84
  headers = [
84
85
  (k.encode("ascii"), v.encode("ascii"))
@@ -86,15 +87,21 @@ def _build_response(response_model: HttpResponse) -> Any:
86
87
  # Skip headers that cause issues
87
88
  if k.lower() not in ("transfer-encoding", "content-encoding")
88
89
  ]
89
-
90
+
90
91
  body = response_model.get_body_bytes()
91
-
92
- # Decompress gzip body if needed (since we removed content-encoding header)
93
- if content_encoding == "gzip" and body:
92
+
93
+ # Decompress body if needed (since we removed content-encoding header)
94
+ if body and content_encoding in ("gzip", "deflate"):
94
95
  try:
95
- body = gzip.decompress(body)
96
- except (gzip.BadGzipFile, OSError):
97
- pass # Not actually gzip, use as-is
96
+ if content_encoding == "gzip":
97
+ body = gzip.decompress(body)
98
+ elif content_encoding == "deflate":
99
+ try:
100
+ body = zlib.decompress(body)
101
+ except zlib.error:
102
+ body = zlib.decompress(body, -zlib.MAX_WBITS)
103
+ except (gzip.BadGzipFile, OSError, zlib.error):
104
+ pass # Not actually compressed, use as-is
98
105
 
99
106
  return Response(
100
107
  status=response_model.status,
@@ -105,16 +112,17 @@ def _build_response(response_model: HttpResponse) -> Any:
105
112
 
106
113
  def _serialize_response(httpcore_response: Any) -> HttpResponse:
107
114
  """Create HttpResponse from httpcore Response object.
108
-
109
- Handles gzip decompression before serialization to ensure
115
+
116
+ Handles gzip/deflate decompression before serialization to ensure
110
117
  body is stored as readable text, not binary.
111
118
  """
112
119
  import gzip
120
+ import zlib
113
121
  from .._models import HttpResponse
114
-
122
+
115
123
  # Get body - should be available after read()/aread()
116
124
  body_bytes = httpcore_response.content
117
-
125
+
118
126
  # Convert headers
119
127
  headers_dict = {}
120
128
  content_encoding = ""
@@ -124,21 +132,24 @@ def _serialize_response(httpcore_response: Any) -> HttpResponse:
124
132
  headers_dict[key] = val
125
133
  if key.lower() == "content-encoding":
126
134
  content_encoding = val.lower()
127
-
128
- # If gzip, decompress body before serialization
129
- if content_encoding == "gzip" and body_bytes:
135
+
136
+ # Decompress body before serialization if needed
137
+ if body_bytes and content_encoding in ("gzip", "deflate"):
130
138
  try:
131
- body_bytes = gzip.decompress(body_bytes)
139
+ if content_encoding == "gzip":
140
+ body_bytes = gzip.decompress(body_bytes)
141
+ elif content_encoding == "deflate":
142
+ try:
143
+ body_bytes = zlib.decompress(body_bytes)
144
+ except zlib.error:
145
+ body_bytes = zlib.decompress(body_bytes, -zlib.MAX_WBITS)
132
146
  # Remove content-encoding header since we decompressed
133
147
  headers_dict = {
134
148
  k: v for k, v in headers_dict.items()
135
149
  if k.lower() != "content-encoding"
136
150
  }
137
- except (gzip.BadGzipFile, OSError):
138
- pass # Not actually gzip, use as-is
139
-
140
- # Determine content type
141
- content_type = headers_dict.get("content-type", headers_dict.get("Content-Type", ""))
151
+ except (gzip.BadGzipFile, OSError, zlib.error):
152
+ pass # Not actually compressed, use as-is
142
153
 
143
154
  # Try to decode as text/JSON
144
155
  body_str = None
@@ -43,6 +43,9 @@ class ConnectionCache:
43
43
  Maintains separate caches for sync and async connections.
44
44
  Connections are created on first access and reused for subsequent calls.
45
45
  All connections are closed when the process exits via atexit handler.
46
+
47
+ Async connections are tracked with their event loop - if the loop changes
48
+ or closes, the cached connection is invalidated automatically.
46
49
  """
47
50
 
48
51
  _instance: Optional["ConnectionCache"] = None
@@ -58,7 +61,8 @@ class ConnectionCache:
58
61
  return
59
62
  self._initialized = True
60
63
  self._sync_connections: Dict[CacheKey, Any] = {}
61
- self._async_connections: Dict[CacheKey, Any] = {}
64
+ # Store (connection, event_loop) tuple for async connections
65
+ self._async_connections: Dict[CacheKey, Tuple[Any, Any]] = {}
62
66
  self._sync_close_funcs: Dict[CacheKey, Any] = {}
63
67
  self._async_close_funcs: Dict[CacheKey, Any] = {}
64
68
 
@@ -90,25 +94,36 @@ class ConnectionCache:
90
94
  async def get_async(
91
95
  self,
92
96
  config: "SqlTestConfig",
93
- connect_coro: Any,
97
+ connect_factory: Any,
94
98
  ) -> Any:
95
99
  """
96
100
  Get or create an async connection.
97
101
 
102
+ Tracks the event loop - if the cached connection was created in a
103
+ different or closed loop, it's invalidated and a new one is created.
104
+
98
105
  Args:
99
106
  config: Database configuration
100
- connect_coro: Coroutine that creates connection
107
+ connect_factory: Callable that returns a coroutine when called
101
108
 
102
109
  Returns:
103
110
  Database connection object
104
111
  """
105
112
  key = CacheKey.from_config(config)
113
+ current_loop = asyncio.get_running_loop()
106
114
 
107
- if key not in self._async_connections:
108
- connection = await connect_coro
109
- self._async_connections[key] = connection
115
+ if key in self._async_connections:
116
+ connection, cached_loop = self._async_connections[key]
117
+ # Check if loop is the same and still running
118
+ if cached_loop is current_loop and not cached_loop.is_closed():
119
+ return connection
120
+ # Loop changed or closed - remove stale connection
121
+ del self._async_connections[key]
110
122
 
111
- return self._async_connections[key]
123
+ # Create new connection and track with current loop
124
+ connection = await connect_factory()
125
+ self._async_connections[key] = (connection, current_loop)
126
+ return connection
112
127
 
113
128
  def register_sync_closer(
114
129
  self,
@@ -150,7 +165,7 @@ class ConnectionCache:
150
165
  This is called by atexit handler which runs in sync context.
151
166
  We need to handle async cleanup carefully.
152
167
  """
153
- for key, connection in list(self._async_connections.items()):
168
+ for key, (connection, _loop) in list(self._async_connections.items()):
154
169
  try:
155
170
  if hasattr(connection, "close"):
156
171
  # Some async connections have sync close()
@@ -174,7 +189,7 @@ class ConnectionCache:
174
189
 
175
190
  async def close_all_async(self) -> None:
176
191
  """Close all cached async connections from async context."""
177
- for key, connection in list(self._async_connections.items()):
192
+ for key, (connection, _loop) in list(self._async_connections.items()):
178
193
  try:
179
194
  if key in self._async_close_funcs:
180
195
  await self._async_close_funcs[key](connection)
@@ -28,7 +28,7 @@ class AiomysqlStrategy(AsyncSqlExecutionStrategy):
28
28
  params = self.build_connection_params(config)
29
29
 
30
30
  connection = await self.get_cached_connection_async(
31
- config, driver.connect(**params)
31
+ config, lambda: driver.connect(**params)
32
32
  )
33
33
  async with connection.cursor() as cursor:
34
34
  # aiomysql doesn't support multi-statement by default
@@ -26,7 +26,7 @@ class AiosqliteStrategy(AsyncSqlExecutionStrategy):
26
26
  database_path = params.get("database", ":memory:")
27
27
 
28
28
  connection = await self.get_cached_connection_async(
29
- config, driver.connect(database_path)
29
+ config, lambda: driver.connect(database_path)
30
30
  )
31
31
  await connection.executescript(sql_content)
32
32
  await connection.commit()
@@ -9,7 +9,11 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  class AsyncpgStrategy(AsyncSqlExecutionStrategy):
12
- """Execution strategy for asyncpg (PostgreSQL async)."""
12
+ """Execution strategy for asyncpg (PostgreSQL async).
13
+
14
+ Uses connection pools instead of single connections to support
15
+ concurrent operations across multiple tests.
16
+ """
13
17
 
14
18
  driver_name = "asyncpg"
15
19
  is_async = True
@@ -23,11 +27,13 @@ class AsyncpgStrategy(AsyncSqlExecutionStrategy):
23
27
  }
24
28
 
25
29
  async def execute_async(self, sql_content: str, config: "SqlTestConfig") -> None:
26
- """Execute SQL using asyncpg."""
30
+ """Execute SQL using asyncpg with connection pool."""
27
31
  driver = self.get_driver_module()
28
32
  params = self.build_connection_params(config)
29
33
 
30
- connection = await self.get_cached_connection_async(
31
- config, driver.connect(**params)
34
+ # Use pool instead of single connection for concurrency support
35
+ pool = await self.get_cached_connection_async(
36
+ config, lambda: driver.create_pool(**params, min_size=1, max_size=5)
32
37
  )
33
- await connection.execute(sql_content)
38
+ async with pool.acquire() as connection:
39
+ await connection.execute(sql_content)
@@ -142,14 +142,20 @@ class AsyncSqlExecutionStrategy(SqlExecutionStrategy):
142
142
  async def get_cached_connection_async(
143
143
  self,
144
144
  config: "SqlTestConfig",
145
- connect_coro: Any,
145
+ connect_factory: Any,
146
146
  ) -> Any:
147
147
  """
148
148
  Get a cached async connection or create a new one.
149
149
 
150
+ Note: Async connections are tied to the event loop. For caching to work,
151
+ pytest must use session-scoped event loop:
152
+
153
+ [tool.pytest.ini_options]
154
+ asyncio_default_fixture_loop_scope = "session"
155
+
150
156
  Args:
151
157
  config: Database configuration
152
- connect_coro: Coroutine that creates connection
158
+ connect_factory: Callable that returns a coroutine when called
153
159
 
154
160
  Returns:
155
161
  Database connection (cached or newly created)
@@ -157,4 +163,4 @@ class AsyncSqlExecutionStrategy(SqlExecutionStrategy):
157
163
  from .._connection_cache import get_connection_cache
158
164
 
159
165
  cache = get_connection_cache()
160
- return await cache.get_async(config, connect_coro)
166
+ return await cache.get_async(config, connect_factory)
@@ -28,7 +28,7 @@ class PsycopgStrategy(AsyncSqlExecutionStrategy):
28
28
  params = self.build_connection_params(config)
29
29
 
30
30
  connection = await self.get_cached_connection_async(
31
- config, driver.AsyncConnection.connect(**params)
31
+ config, lambda: driver.AsyncConnection.connect(**params)
32
32
  )
33
33
  await connection.execute(sql_content)
34
34
  await connection.commit()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixturify
3
- Version: 0.1.11
3
+ Version: 0.1.12
4
4
  Summary: A collection of convenient testing utilities for Python
5
5
  Project-URL: Homepage, https://github.com/eleven-sea/pytools
6
6
  Project-URL: Repository, https://github.com/eleven-sea/pytools
@@ -1,4 +1,4 @@
1
- fixturify/__init__.py,sha256=KOtcjgkSXK30bgl49i_edSu2yUNDqWAzVubxosMBqFQ,485
1
+ fixturify/__init__.py,sha256=mVvtOZMpeQKxYJ3TR0GtY0HjT6CKi2RgWATp0ErJX-A,485
2
2
  fixturify/_utils/__init__.py,sha256=Ago2DIAS0UgTvVRxWGisxPoawDFNAB5mbjnC0hYsmNw,334
3
3
  fixturify/_utils/_constants.py,sha256=CYkqgI-ljyMcntBUP8ZCQmx7JSsLqyNxzIwNfxm2XkA,453
4
4
  fixturify/_utils/_fixture_discovery.py,sha256=u9hfoNdbLz35-X7h6fwdGuQQ8edryGNWfg_ba5i8Nq8,5185
@@ -13,12 +13,12 @@ fixturify/http_d/_mock_context.py,sha256=cqKzlaGPDuAPNF1k1M3tVUjDU9SUlgICPe0d2YJ
13
13
  fixturify/http_d/_models.py,sha256=4hz9S_OTOE55zsCewfTabaR5fDjKJjPTuO_hLcfq8bk,6372
14
14
  fixturify/http_d/_patcher.py,sha256=KTMc9pDcr7yA82hHe4aKjP9clmwSOZTwBcsiQm1S8Cc,18700
15
15
  fixturify/http_d/_player.py,sha256=4ayKgDOans7AXX8PIfWxn2nxauQBswIUSY2jvL2eIlU,7275
16
- fixturify/http_d/_recorder.py,sha256=VQVCxIAxeDhXo_uZ3b1Yr7TKw5-jvUEY-ZXO_0DVr0c,40633
16
+ fixturify/http_d/_recorder.py,sha256=mZEu41SAvvD02tOOePy2K20-fxg88OU5VpqfnXX8vk8,42600
17
17
  fixturify/http_d/_utils.py,sha256=FJFZC8IS2MzLbPo6D0CUizcJ-LYXc0fVoSJ-04bL5aQ,5799
18
18
  fixturify/http_d/_stubs/__init__.py,sha256=sFcWbdqhnHNiYLfKfrrLt6wjGPWo3J-byTDZd2z41wk,171
19
19
  fixturify/http_d/_stubs/_aiohttp.py,sha256=c-zGo78_MHfJz_2OsSv0bGnFAR1NgGkACMuzrKd-qII,6672
20
20
  fixturify/http_d/_stubs/_connection.py,sha256=Z_g4C_WWL2ErWO26xV3rJqZU48slabtX_al-_LKmGIk,16253
21
- fixturify/http_d/_stubs/_httpcore.py,sha256=WR_KZGInuVqoBBRML_T6bGe5LEMjbW4ElRem5pz6HiE,9007
21
+ fixturify/http_d/_stubs/_httpcore.py,sha256=vBpvcaf81PVwMDbL0Gxa6uvokV9k4_gGyJybocxNBRE,9473
22
22
  fixturify/http_d/_stubs/_tornado.py,sha256=Vm-hETi01xGIjVVvYmVoIOXhzSWAukae48Tzmy0afFY,3219
23
23
  fixturify/json_assert/__init__.py,sha256=eDwsoGZPMDparbzKAJ2xXorVsEVPMcbHMXMfue3a3YM,375
24
24
  fixturify/json_assert/_actual_saver.py,sha256=_BXTI2CScIUJ7UeeU1-mIKssaDrHGldlFHDtq1o_i80,2355
@@ -51,22 +51,22 @@ fixturify/read_d/_decorator.py,sha256=7uKRZ-WkGHxSNf87qlC7u-0PfiNrlZHbnQ-jWXrk5n
51
51
  fixturify/read_d/_fixture_loader.py,sha256=9pXJxLOEcdLylZup41wXuok6n7bHYExPBqvFZP2Fya0,2857
52
52
  fixturify/sql_d/__init__.py,sha256=Y18FgZFI_z-HUAdQ6uHOppMDZC1W5hYGZhC3ZJZ5pB0,332
53
53
  fixturify/sql_d/_config.py,sha256=W3RL1UW9WrYWNHu2FqcOW22Q_loYLi9htXsqARALH6s,762
54
- fixturify/sql_d/_connection_cache.py,sha256=5J0RLWl-6Aoyc4Ybx5FthsjeN_SA4flWgDrl6L2bODw,7172
54
+ fixturify/sql_d/_connection_cache.py,sha256=SEzfcLsj3aX2Rz7cVKCatol7m4cEKFEXqKuE4gw-SGI,8031
55
55
  fixturify/sql_d/_decorator.py,sha256=9ynQlODhU5m4d29DjMQMpuMG_Fd0jikhzuMKkNDXUmA,12531
56
56
  fixturify/sql_d/_driver_registry.py,sha256=Fwxt86Gv2UbnO7V_FgzaGc1UvCzd8QV5UtHiRdahlNs,3349
57
57
  fixturify/sql_d/_executor.py,sha256=iEizcgSKrShh1CHwiVjkM1Xjs1Iiw7WESnzborAQpqY,2271
58
58
  fixturify/sql_d/_fixture_discovery.py,sha256=17m3RmJvSH8ygdZoDbkm7RIdcewe5wEyIx6X_40MxjY,1635
59
59
  fixturify/sql_d/_phase.py,sha256=zQv2YwRkXSB4Kdeqrcp5BELplWHJVgQd_0wlcjS2eqY,228
60
60
  fixturify/sql_d/_strategies/__init__.py,sha256=36IdX6pFdBgZJ1-Cn3HQfwhKgKp-bB9g4qvN3KwoncQ,323
61
- fixturify/sql_d/_strategies/_aiomysql.py,sha256=uo0y27kfl82c9OqhWSLsx1hHN6ow0gCQLiwi5Bwi5iY,2060
62
- fixturify/sql_d/_strategies/_aiosqlite.py,sha256=gccGZKYA17JZXG7P4o6NrgHK77BGgErz0XDKWGMAY3A,958
63
- fixturify/sql_d/_strategies/_asyncpg.py,sha256=3TW0XW6M3jYJGfn8KeZtgLP6XC3jSi2anGVFAn5HCGA,930
64
- fixturify/sql_d/_strategies/_base.py,sha256=4g-7-W1muTcQ5H4h3pN64m3MiuUou1wMcQeGLohg_4Y,4705
61
+ fixturify/sql_d/_strategies/_aiomysql.py,sha256=4_E0iuRJW0cEikOi4Oh0JkskViEWI6Z6BwLezTzUnSg,2068
62
+ fixturify/sql_d/_strategies/_aiosqlite.py,sha256=NHsnpL2RR7oEnCKwZfhsroMPPOJtj0hhGydbbAypSpk,966
63
+ fixturify/sql_d/_strategies/_asyncpg.py,sha256=JdLS8ckRieEwgSWpq9omQW_Vx0gxR7_ZIsFpiwqRhG4,1228
64
+ fixturify/sql_d/_strategies/_base.py,sha256=KskvsxFpy9DlNpmteLaZ-dgAeJ6FuBjo4qJTYlKMNQk,4957
65
65
  fixturify/sql_d/_strategies/_mysql.py,sha256=4MG0Pn7GWNYaakqAbFkNT3xOKKJh4DJ9JshHdbJwgts,2389
66
- fixturify/sql_d/_strategies/_psycopg.py,sha256=8uD-mkeQKuVW9wqnfizw_FJ67MTQ80A1AP8XGvVMMIQ,995
66
+ fixturify/sql_d/_strategies/_psycopg.py,sha256=rs86H2FnSnFSCMruRF5xjg7tdMsMlYY5Ew7k_J-gpho,1003
67
67
  fixturify/sql_d/_strategies/_psycopg2.py,sha256=2M0fmYtLxFxJXHsgxomaEEzPVS9u0TFs6u4ldIINy08,1042
68
68
  fixturify/sql_d/_strategies/_registry.py,sha256=ddQpp9cU4SHA2OylsPpMBMarLyQKmV-4Ge4X395tg1Q,3346
69
69
  fixturify/sql_d/_strategies/_sqlite.py,sha256=ZXZwdgzcMdbrE2dUMjbBJD4m1gLec5nAM6DThs108_g,1002
70
- fixturify-0.1.11.dist-info/METADATA,sha256=4B1_WgTgeN43Lrf1RX_Q69j9mQQaN-h6lM0lCTwp2tI,2979
71
- fixturify-0.1.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
72
- fixturify-0.1.11.dist-info/RECORD,,
70
+ fixturify-0.1.12.dist-info/METADATA,sha256=hrmOdh-BmfKlt_NfJnjqF_7donwxNu3GVj4zmnyMgTY,2979
71
+ fixturify-0.1.12.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
72
+ fixturify-0.1.12.dist-info/RECORD,,