pytest-neon 2.1.4__py3-none-any.whl → 2.2.0__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.
- pytest_neon/__init__.py +1 -1
- pytest_neon/plugin.py +256 -62
- {pytest_neon-2.1.4.dist-info → pytest_neon-2.2.0.dist-info}/METADATA +16 -1
- pytest_neon-2.2.0.dist-info/RECORD +8 -0
- pytest_neon-2.1.4.dist-info/RECORD +0 -8
- {pytest_neon-2.1.4.dist-info → pytest_neon-2.2.0.dist-info}/WHEEL +0 -0
- {pytest_neon-2.1.4.dist-info → pytest_neon-2.2.0.dist-info}/entry_points.txt +0 -0
- {pytest_neon-2.1.4.dist-info → pytest_neon-2.2.0.dist-info}/licenses/LICENSE +0 -0
pytest_neon/__init__.py
CHANGED
pytest_neon/plugin.py
CHANGED
|
@@ -37,25 +37,193 @@ from __future__ import annotations
|
|
|
37
37
|
|
|
38
38
|
import contextlib
|
|
39
39
|
import os
|
|
40
|
+
import random
|
|
40
41
|
import time
|
|
41
42
|
import warnings
|
|
42
|
-
from collections.abc import Generator
|
|
43
|
+
from collections.abc import Callable, Generator
|
|
43
44
|
from dataclasses import dataclass
|
|
44
45
|
from datetime import datetime, timedelta, timezone
|
|
45
|
-
from typing import Any
|
|
46
|
+
from typing import Any, TypeVar
|
|
46
47
|
|
|
47
48
|
import pytest
|
|
48
49
|
import requests
|
|
49
50
|
from neon_api import NeonAPI
|
|
51
|
+
from neon_api.exceptions import NeonAPIError
|
|
50
52
|
from neon_api.schema import EndpointState
|
|
51
53
|
|
|
54
|
+
T = TypeVar("T")
|
|
55
|
+
|
|
52
56
|
# Default branch expiry in seconds (10 minutes)
|
|
53
57
|
DEFAULT_BRANCH_EXPIRY_SECONDS = 600
|
|
54
58
|
|
|
59
|
+
# Rate limit retry configuration
|
|
60
|
+
# See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
61
|
+
# Neon limits: 700 requests/minute (~11/sec), burst up to 40/sec per route
|
|
62
|
+
_RATE_LIMIT_BASE_DELAY = 4.0 # seconds
|
|
63
|
+
_RATE_LIMIT_MAX_TOTAL_DELAY = 90.0 # 1.5 minutes total cap
|
|
64
|
+
_RATE_LIMIT_JITTER_FACTOR = 0.25 # +/- 25% jitter
|
|
65
|
+
_RATE_LIMIT_MAX_ATTEMPTS = 10 # Maximum number of retry attempts
|
|
66
|
+
|
|
55
67
|
# Sentinel value to detect when neon_apply_migrations was not overridden
|
|
56
68
|
_MIGRATIONS_NOT_DEFINED = object()
|
|
57
69
|
|
|
58
70
|
|
|
71
|
+
class NeonRateLimitError(Exception):
|
|
72
|
+
"""Raised when Neon API rate limit is exceeded and retries are exhausted."""
|
|
73
|
+
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _calculate_retry_delay(
|
|
78
|
+
attempt: int,
|
|
79
|
+
base_delay: float = _RATE_LIMIT_BASE_DELAY,
|
|
80
|
+
jitter_factor: float = _RATE_LIMIT_JITTER_FACTOR,
|
|
81
|
+
) -> float:
|
|
82
|
+
"""
|
|
83
|
+
Calculate delay for a retry attempt with exponential backoff and jitter.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
attempt: The retry attempt number (0-indexed)
|
|
87
|
+
base_delay: Base delay in seconds
|
|
88
|
+
jitter_factor: Jitter factor (0.25 means +/- 25%)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Delay in seconds with jitter applied
|
|
92
|
+
"""
|
|
93
|
+
# Exponential backoff: base_delay * 2^attempt
|
|
94
|
+
delay = base_delay * (2**attempt)
|
|
95
|
+
|
|
96
|
+
# Apply jitter: delay * (1 +/- jitter_factor)
|
|
97
|
+
jitter = delay * jitter_factor * (2 * random.random() - 1)
|
|
98
|
+
return delay + jitter
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _is_rate_limit_error(exc: Exception) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Check if an exception indicates a rate limit (429) error.
|
|
104
|
+
|
|
105
|
+
Handles both requests.HTTPError (with response object) and NeonAPIError
|
|
106
|
+
(which only has the error text, not the response object).
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
exc: The exception to check
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if this is a rate limit error, False otherwise
|
|
113
|
+
"""
|
|
114
|
+
# Check NeonAPIError first - it inherits from HTTPError but doesn't have
|
|
115
|
+
# a response object, so we need to check the error text
|
|
116
|
+
if isinstance(exc, NeonAPIError):
|
|
117
|
+
# NeonAPIError doesn't preserve the response object, only the text
|
|
118
|
+
# Check for rate limit indicators in the error message
|
|
119
|
+
# Note: We use "too many requests" specifically to avoid false positives
|
|
120
|
+
# from errors like "too many connections" or "too many rows"
|
|
121
|
+
error_text = str(exc).lower()
|
|
122
|
+
return (
|
|
123
|
+
"429" in error_text
|
|
124
|
+
or "rate limit" in error_text
|
|
125
|
+
or "too many requests" in error_text
|
|
126
|
+
)
|
|
127
|
+
if isinstance(exc, requests.HTTPError):
|
|
128
|
+
return exc.response is not None and exc.response.status_code == 429
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _get_retry_after_from_error(exc: Exception) -> float | None:
|
|
133
|
+
"""
|
|
134
|
+
Extract Retry-After header value from an exception if available.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
exc: The exception to check
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The Retry-After value in seconds, or None if not available
|
|
141
|
+
"""
|
|
142
|
+
if isinstance(exc, requests.HTTPError) and exc.response is not None:
|
|
143
|
+
retry_after = exc.response.headers.get("Retry-After")
|
|
144
|
+
if retry_after:
|
|
145
|
+
try:
|
|
146
|
+
return float(retry_after)
|
|
147
|
+
except ValueError:
|
|
148
|
+
pass
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _retry_on_rate_limit(
|
|
153
|
+
operation: Callable[[], T],
|
|
154
|
+
operation_name: str,
|
|
155
|
+
base_delay: float = _RATE_LIMIT_BASE_DELAY,
|
|
156
|
+
max_total_delay: float = _RATE_LIMIT_MAX_TOTAL_DELAY,
|
|
157
|
+
jitter_factor: float = _RATE_LIMIT_JITTER_FACTOR,
|
|
158
|
+
max_attempts: int = _RATE_LIMIT_MAX_ATTEMPTS,
|
|
159
|
+
) -> T:
|
|
160
|
+
"""
|
|
161
|
+
Execute an operation with retry logic for rate limit (429) errors.
|
|
162
|
+
|
|
163
|
+
Uses exponential backoff with jitter. Retries until the operation succeeds,
|
|
164
|
+
the total delay exceeds max_total_delay, or max_attempts is reached.
|
|
165
|
+
|
|
166
|
+
See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
operation: Callable that may raise requests.HTTPError or NeonAPIError
|
|
170
|
+
operation_name: Human-readable name for error messages
|
|
171
|
+
base_delay: Base delay in seconds for first retry
|
|
172
|
+
max_total_delay: Maximum total delay across all retries
|
|
173
|
+
jitter_factor: Jitter factor for randomization
|
|
174
|
+
max_attempts: Maximum number of retry attempts
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The result of the operation
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
NeonRateLimitError: If rate limit retries are exhausted
|
|
181
|
+
requests.HTTPError: For non-429 HTTP errors
|
|
182
|
+
NeonAPIError: For non-429 API errors
|
|
183
|
+
Exception: For other errors from the operation
|
|
184
|
+
"""
|
|
185
|
+
total_delay = 0.0
|
|
186
|
+
attempt = 0
|
|
187
|
+
|
|
188
|
+
while True:
|
|
189
|
+
try:
|
|
190
|
+
return operation()
|
|
191
|
+
except (requests.HTTPError, NeonAPIError) as e:
|
|
192
|
+
if _is_rate_limit_error(e):
|
|
193
|
+
# Check for Retry-After header (may be added by Neon in future)
|
|
194
|
+
retry_after = _get_retry_after_from_error(e)
|
|
195
|
+
if retry_after is not None:
|
|
196
|
+
# Ensure minimum delay to prevent infinite loops if Retry-After is 0
|
|
197
|
+
delay = max(retry_after, 0.1)
|
|
198
|
+
else:
|
|
199
|
+
delay = _calculate_retry_delay(attempt, base_delay, jitter_factor)
|
|
200
|
+
|
|
201
|
+
# Check if we've exceeded max total delay
|
|
202
|
+
if total_delay + delay > max_total_delay:
|
|
203
|
+
raise NeonRateLimitError(
|
|
204
|
+
f"Rate limit exceeded for {operation_name}. "
|
|
205
|
+
f"Max total delay ({max_total_delay:.1f}s) reached after "
|
|
206
|
+
f"{attempt + 1} attempts. "
|
|
207
|
+
f"See: https://api-docs.neon.tech/reference/api-rate-limiting"
|
|
208
|
+
) from e
|
|
209
|
+
|
|
210
|
+
# Check if we've exceeded max attempts
|
|
211
|
+
attempt += 1
|
|
212
|
+
if attempt >= max_attempts:
|
|
213
|
+
raise NeonRateLimitError(
|
|
214
|
+
f"Rate limit exceeded for {operation_name}. "
|
|
215
|
+
f"Max attempts ({max_attempts}) reached after "
|
|
216
|
+
f"{total_delay:.1f}s total delay. "
|
|
217
|
+
f"See: https://api-docs.neon.tech/reference/api-rate-limiting"
|
|
218
|
+
) from e
|
|
219
|
+
|
|
220
|
+
time.sleep(delay)
|
|
221
|
+
total_delay += delay
|
|
222
|
+
else:
|
|
223
|
+
# Non-429 error, re-raise immediately
|
|
224
|
+
raise
|
|
225
|
+
|
|
226
|
+
|
|
59
227
|
def _get_xdist_worker_id() -> str:
|
|
60
228
|
"""
|
|
61
229
|
Get the pytest-xdist worker ID, or "main" if not running under xdist.
|
|
@@ -166,7 +334,12 @@ def _get_default_branch_id(neon: NeonAPI, project_id: str) -> str | None:
|
|
|
166
334
|
The branch ID of the default branch, or None if not found.
|
|
167
335
|
"""
|
|
168
336
|
try:
|
|
169
|
-
|
|
337
|
+
# Wrap in retry logic to handle rate limits
|
|
338
|
+
# See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
339
|
+
response = _retry_on_rate_limit(
|
|
340
|
+
lambda: neon.branches(project_id=project_id),
|
|
341
|
+
operation_name="list_branches",
|
|
342
|
+
)
|
|
170
343
|
for branch in response.branches:
|
|
171
344
|
# Check both 'default' and 'primary' flags for compatibility
|
|
172
345
|
if getattr(branch, "default", False) or getattr(branch, "primary", False):
|
|
@@ -375,10 +548,15 @@ def _create_neon_branch(
|
|
|
375
548
|
branch_config["expires_at"] = expires_at.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
376
549
|
|
|
377
550
|
# Create branch with compute endpoint
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
551
|
+
# Wrap in retry logic to handle rate limits
|
|
552
|
+
# See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
553
|
+
result = _retry_on_rate_limit(
|
|
554
|
+
lambda: neon.branch_create(
|
|
555
|
+
project_id=project_id,
|
|
556
|
+
branch=branch_config,
|
|
557
|
+
endpoints=[{"type": "read_write"}],
|
|
558
|
+
),
|
|
559
|
+
operation_name="branch_create",
|
|
382
560
|
)
|
|
383
561
|
|
|
384
562
|
branch = result.branch
|
|
@@ -402,8 +580,11 @@ def _create_neon_branch(
|
|
|
402
580
|
waited = 0.0
|
|
403
581
|
|
|
404
582
|
while True:
|
|
405
|
-
|
|
406
|
-
|
|
583
|
+
# Wrap in retry logic to handle rate limits during polling
|
|
584
|
+
# See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
585
|
+
endpoint_response = _retry_on_rate_limit(
|
|
586
|
+
lambda: neon.endpoint(project_id=project_id, endpoint_id=endpoint_id),
|
|
587
|
+
operation_name="endpoint_status",
|
|
407
588
|
)
|
|
408
589
|
endpoint = endpoint_response.endpoint
|
|
409
590
|
state = endpoint.current_state
|
|
@@ -436,10 +617,15 @@ def _create_neon_branch(
|
|
|
436
617
|
|
|
437
618
|
# Reset password to get the password value
|
|
438
619
|
# (newly created branches don't expose password)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
620
|
+
# Wrap in retry logic to handle rate limits
|
|
621
|
+
# See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
622
|
+
password_response = _retry_on_rate_limit(
|
|
623
|
+
lambda: neon.role_password_reset(
|
|
624
|
+
project_id=project_id,
|
|
625
|
+
branch_id=branch.id,
|
|
626
|
+
role_name=role_name,
|
|
627
|
+
),
|
|
628
|
+
operation_name="role_password_reset",
|
|
443
629
|
)
|
|
444
630
|
password = password_response.role.password
|
|
445
631
|
|
|
@@ -472,30 +658,34 @@ def _create_neon_branch(
|
|
|
472
658
|
# Cleanup: delete branch unless --neon-keep-branches was specified
|
|
473
659
|
if not keep_branches:
|
|
474
660
|
try:
|
|
475
|
-
|
|
661
|
+
# Wrap in retry logic to handle rate limits
|
|
662
|
+
# See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
663
|
+
_retry_on_rate_limit(
|
|
664
|
+
lambda: neon.branch_delete(
|
|
665
|
+
project_id=project_id, branch_id=branch.id
|
|
666
|
+
),
|
|
667
|
+
operation_name="branch_delete",
|
|
668
|
+
)
|
|
476
669
|
except Exception as e:
|
|
477
670
|
# Log but don't fail tests due to cleanup issues
|
|
478
|
-
import warnings
|
|
479
|
-
|
|
480
671
|
warnings.warn(
|
|
481
672
|
f"Failed to delete Neon branch {branch.id}: {e}",
|
|
482
673
|
stacklevel=2,
|
|
483
674
|
)
|
|
484
675
|
|
|
485
676
|
|
|
486
|
-
def _reset_branch_to_parent(
|
|
487
|
-
branch: NeonBranch, api_key: str, max_retries: int = 3
|
|
488
|
-
) -> None:
|
|
677
|
+
def _reset_branch_to_parent(branch: NeonBranch, api_key: str) -> None:
|
|
489
678
|
"""Reset a branch to its parent's state using the Neon API.
|
|
490
679
|
|
|
491
|
-
Uses exponential backoff retry logic to handle
|
|
492
|
-
|
|
493
|
-
|
|
680
|
+
Uses exponential backoff retry logic with jitter to handle rate limit (429)
|
|
681
|
+
errors. After initiating the restore, polls the operation status until it
|
|
682
|
+
completes.
|
|
683
|
+
|
|
684
|
+
See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
494
685
|
|
|
495
686
|
Args:
|
|
496
687
|
branch: The branch to reset
|
|
497
688
|
api_key: Neon API key
|
|
498
|
-
max_retries: Maximum number of retry attempts (default: 3)
|
|
499
689
|
"""
|
|
500
690
|
if not branch.parent_id:
|
|
501
691
|
raise RuntimeError(f"Branch {branch.branch_id} has no parent - cannot reset")
|
|
@@ -509,41 +699,31 @@ def _reset_branch_to_parent(
|
|
|
509
699
|
"Content-Type": "application/json",
|
|
510
700
|
}
|
|
511
701
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
return # Success
|
|
538
|
-
except requests.RequestException as e:
|
|
539
|
-
last_error = e
|
|
540
|
-
if attempt < max_retries:
|
|
541
|
-
# Exponential backoff: 1s, 2s, 4s
|
|
542
|
-
wait_time = 2**attempt
|
|
543
|
-
time.sleep(wait_time)
|
|
544
|
-
|
|
545
|
-
# All retries exhausted
|
|
546
|
-
raise last_error # type: ignore[misc]
|
|
702
|
+
def do_restore() -> dict[str, Any]:
|
|
703
|
+
response = requests.post(
|
|
704
|
+
restore_url,
|
|
705
|
+
headers=headers,
|
|
706
|
+
json={"source_branch_id": branch.parent_id},
|
|
707
|
+
timeout=30,
|
|
708
|
+
)
|
|
709
|
+
response.raise_for_status()
|
|
710
|
+
return response.json()
|
|
711
|
+
|
|
712
|
+
# Wrap in retry logic to handle rate limits
|
|
713
|
+
# See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
714
|
+
data = _retry_on_rate_limit(do_restore, operation_name="branch_restore")
|
|
715
|
+
operations = data.get("operations", [])
|
|
716
|
+
|
|
717
|
+
# The restore API returns operations that run asynchronously.
|
|
718
|
+
# We must wait for operations to complete before the next test
|
|
719
|
+
# starts, otherwise connections may fail during the restore.
|
|
720
|
+
if operations:
|
|
721
|
+
_wait_for_operations(
|
|
722
|
+
project_id=branch.project_id,
|
|
723
|
+
operations=operations,
|
|
724
|
+
headers=headers,
|
|
725
|
+
base_url=base_url,
|
|
726
|
+
)
|
|
547
727
|
|
|
548
728
|
|
|
549
729
|
def _wait_for_operations(
|
|
@@ -556,6 +736,9 @@ def _wait_for_operations(
|
|
|
556
736
|
) -> None:
|
|
557
737
|
"""Wait for Neon operations to complete.
|
|
558
738
|
|
|
739
|
+
Handles rate limit (429) errors with exponential backoff retry.
|
|
740
|
+
See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
741
|
+
|
|
559
742
|
Args:
|
|
560
743
|
project_id: The Neon project ID
|
|
561
744
|
operations: List of operation dicts from the API response
|
|
@@ -589,10 +772,21 @@ def _wait_for_operations(
|
|
|
589
772
|
still_pending = []
|
|
590
773
|
for op_id in pending_op_ids:
|
|
591
774
|
op_url = f"{base_url}/projects/{project_id}/operations/{op_id}"
|
|
592
|
-
|
|
593
|
-
|
|
775
|
+
|
|
776
|
+
def get_operation_status(url: str = op_url) -> dict[str, Any]:
|
|
777
|
+
"""Fetch operation status. Default arg captures url by value."""
|
|
778
|
+
response = requests.get(url, headers=headers, timeout=10)
|
|
594
779
|
response.raise_for_status()
|
|
595
|
-
|
|
780
|
+
return response.json()
|
|
781
|
+
|
|
782
|
+
try:
|
|
783
|
+
# Wrap in retry logic to handle rate limits
|
|
784
|
+
# See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
785
|
+
result = _retry_on_rate_limit(
|
|
786
|
+
get_operation_status,
|
|
787
|
+
operation_name=f"operation_status({op_id})",
|
|
788
|
+
)
|
|
789
|
+
op_data = result.get("operation", {})
|
|
596
790
|
status = op_data.get("status")
|
|
597
791
|
|
|
598
792
|
if status == "failed":
|
|
@@ -601,7 +795,7 @@ def _wait_for_operations(
|
|
|
601
795
|
if status not in ("finished", "skipped", "cancelled"):
|
|
602
796
|
still_pending.append(op_id)
|
|
603
797
|
except requests.RequestException:
|
|
604
|
-
# On network error, assume still pending and retry
|
|
798
|
+
# On network error (non-429), assume still pending and retry
|
|
605
799
|
still_pending.append(op_id)
|
|
606
800
|
|
|
607
801
|
pending_op_ids = still_pending
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-neon
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Pytest plugin for Neon database branch isolation in tests
|
|
5
5
|
Project-URL: Homepage, https://github.com/ZainRizvi/pytest-neon
|
|
6
6
|
Project-URL: Repository, https://github.com/ZainRizvi/pytest-neon
|
|
@@ -489,6 +489,21 @@ The `neon_branch_readwrite` fixture uses Neon's branch restore API to reset data
|
|
|
489
489
|
|
|
490
490
|
This is similar to database transactions but at the branch level.
|
|
491
491
|
|
|
492
|
+
## Branch Naming
|
|
493
|
+
|
|
494
|
+
Branches are automatically named to help identify their source:
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
pytest-[git-branch]-[random]-[suffix]
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Examples:**
|
|
501
|
+
- `pytest-main-a1b2-migrated` - Migration branch from `main`
|
|
502
|
+
- `pytest-feature-auth-c3d4-test-main` - Test branch from `feature/auth`
|
|
503
|
+
- `pytest-a1b2-migrated` - When not in a git repo
|
|
504
|
+
|
|
505
|
+
The git branch name is sanitized (only `a-z`, `0-9`, `-`, `_` allowed) and truncated to 15 characters. This makes it easy to identify orphaned branches in the Neon console.
|
|
506
|
+
|
|
492
507
|
## Parallel Test Execution (pytest-xdist)
|
|
493
508
|
|
|
494
509
|
This plugin supports parallel test execution with [pytest-xdist](https://pytest-xdist.readthedocs.io/). Each xdist worker automatically gets its own isolated branch.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pytest_neon/__init__.py,sha256=-HXVGp38c6Ddmq90i5eo6OFxKVzBo3igJ6R3DKA8A7M,398
|
|
2
|
+
pytest_neon/plugin.py,sha256=8y5LYvQossHJlPpJdh0DdZj8tFcO8OY45MMMfQytcRA,49269
|
|
3
|
+
pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
pytest_neon-2.2.0.dist-info/METADATA,sha256=dMgATSjnaUH1gH1posidEa99b_IE30ikeVsRsYC3oE0,19237
|
|
5
|
+
pytest_neon-2.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
pytest_neon-2.2.0.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
|
|
7
|
+
pytest_neon-2.2.0.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
|
|
8
|
+
pytest_neon-2.2.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pytest_neon/__init__.py,sha256=bWWilGWaSJW2ofj0YsKqdC7r972s0c2Ar5DcE1Tn0Oc,398
|
|
2
|
-
pytest_neon/plugin.py,sha256=A9qv1mv0CXez-SDAWruzVxjRFp0YdSWkU-8j9TWV_LA,41770
|
|
3
|
-
pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
pytest_neon-2.1.4.dist-info/METADATA,sha256=dmGCHSNi32pEA5wG6fZhjoFrcirza1kO16RdRSmyEJs,18734
|
|
5
|
-
pytest_neon-2.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
-
pytest_neon-2.1.4.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
|
|
7
|
-
pytest_neon-2.1.4.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
|
|
8
|
-
pytest_neon-2.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|