pytest-neon 2.3.0__py3-none-any.whl → 2.3.2__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 +68 -40
- {pytest_neon-2.3.0.dist-info → pytest_neon-2.3.2.dist-info}/METADATA +1 -1
- pytest_neon-2.3.2.dist-info/RECORD +8 -0
- pytest_neon-2.3.0.dist-info/RECORD +0 -8
- {pytest_neon-2.3.0.dist-info → pytest_neon-2.3.2.dist-info}/WHEEL +0 -0
- {pytest_neon-2.3.0.dist-info → pytest_neon-2.3.2.dist-info}/entry_points.txt +0 -0
- {pytest_neon-2.3.0.dist-info → pytest_neon-2.3.2.dist-info}/licenses/LICENSE +0 -0
pytest_neon/__init__.py
CHANGED
pytest_neon/plugin.py
CHANGED
|
@@ -295,6 +295,48 @@ def _get_git_branch_name() -> str | None:
|
|
|
295
295
|
return None
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
def _extract_password_from_connection_string(connection_string: str) -> str:
|
|
299
|
+
"""Extract password from a PostgreSQL connection string."""
|
|
300
|
+
# Format: postgresql://user:password@host/db?params
|
|
301
|
+
from urllib.parse import urlparse
|
|
302
|
+
|
|
303
|
+
parsed = urlparse(connection_string)
|
|
304
|
+
if parsed.password:
|
|
305
|
+
return parsed.password
|
|
306
|
+
raise ValueError(f"No password found in connection string: {connection_string}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _reveal_role_password(
|
|
310
|
+
api_key: str, project_id: str, branch_id: str, role_name: str
|
|
311
|
+
) -> str:
|
|
312
|
+
"""
|
|
313
|
+
Get the password for a role WITHOUT resetting it.
|
|
314
|
+
|
|
315
|
+
Uses Neon's reveal_password API endpoint (GET request).
|
|
316
|
+
|
|
317
|
+
Note: The neon-api library has a bug where it uses POST instead of GET,
|
|
318
|
+
so we make the request directly.
|
|
319
|
+
"""
|
|
320
|
+
url = (
|
|
321
|
+
f"https://console.neon.tech/api/v2/projects/{project_id}"
|
|
322
|
+
f"/branches/{branch_id}/roles/{role_name}/reveal_password"
|
|
323
|
+
)
|
|
324
|
+
headers = {
|
|
325
|
+
"Authorization": f"Bearer {api_key}",
|
|
326
|
+
"Accept": "application/json",
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
response = requests.get(url, headers=headers, timeout=30)
|
|
330
|
+
try:
|
|
331
|
+
response.raise_for_status()
|
|
332
|
+
except requests.exceptions.HTTPError:
|
|
333
|
+
# Wrap in NeonAPIError for consistent error handling
|
|
334
|
+
raise NeonAPIError(response.text) from None
|
|
335
|
+
|
|
336
|
+
data = response.json()
|
|
337
|
+
return data["password"]
|
|
338
|
+
|
|
339
|
+
|
|
298
340
|
def _get_schema_fingerprint(connection_string: str) -> tuple[tuple[Any, ...], ...]:
|
|
299
341
|
"""
|
|
300
342
|
Get a fingerprint of the database schema for change detection.
|
|
@@ -497,7 +539,7 @@ class NeonBranchManager:
|
|
|
497
539
|
)
|
|
498
540
|
|
|
499
541
|
# Get password
|
|
500
|
-
connection_string = self.
|
|
542
|
+
connection_string = self._get_password_and_build_connection_string(
|
|
501
543
|
branch.id, host
|
|
502
544
|
)
|
|
503
545
|
|
|
@@ -537,9 +579,15 @@ class NeonBranchManager:
|
|
|
537
579
|
endpoint_id = result.endpoint.id
|
|
538
580
|
host = self._wait_for_endpoint(endpoint_id)
|
|
539
581
|
|
|
540
|
-
#
|
|
541
|
-
|
|
542
|
-
|
|
582
|
+
# Reuse the password from the parent branch's connection string.
|
|
583
|
+
# DO NOT call role_password_reset here - it would invalidate the
|
|
584
|
+
# password used by the parent branch's read_write endpoint, breaking
|
|
585
|
+
# any existing connections (especially in xdist where other workers
|
|
586
|
+
# may be using the cached connection string).
|
|
587
|
+
password = _extract_password_from_connection_string(branch.connection_string)
|
|
588
|
+
connection_string = (
|
|
589
|
+
f"postgresql://{self.config.role_name}:{password}@{host}/"
|
|
590
|
+
f"{self.config.database_name}?sslmode=require"
|
|
543
591
|
)
|
|
544
592
|
|
|
545
593
|
return NeonBranch(
|
|
@@ -615,19 +663,19 @@ class NeonBranchManager:
|
|
|
615
663
|
time.sleep(poll_interval)
|
|
616
664
|
waited += poll_interval
|
|
617
665
|
|
|
618
|
-
def
|
|
666
|
+
def _get_password_and_build_connection_string(
|
|
619
667
|
self, branch_id: str, host: str
|
|
620
668
|
) -> str:
|
|
621
|
-
"""
|
|
622
|
-
|
|
623
|
-
lambda:
|
|
669
|
+
"""Get role password (without resetting) and build connection string."""
|
|
670
|
+
password = _retry_on_rate_limit(
|
|
671
|
+
lambda: _reveal_role_password(
|
|
672
|
+
api_key=self.config.api_key,
|
|
624
673
|
project_id=self.config.project_id,
|
|
625
674
|
branch_id=branch_id,
|
|
626
675
|
role_name=self.config.role_name,
|
|
627
676
|
),
|
|
628
|
-
operation_name="
|
|
677
|
+
operation_name="role_password_reveal",
|
|
629
678
|
)
|
|
630
|
-
password = password_response.role.password
|
|
631
679
|
|
|
632
680
|
return (
|
|
633
681
|
f"postgresql://{self.config.role_name}:{password}@{host}/"
|
|
@@ -1030,31 +1078,17 @@ def _create_neon_branch(
|
|
|
1030
1078
|
|
|
1031
1079
|
host = endpoint.host
|
|
1032
1080
|
|
|
1033
|
-
#
|
|
1034
|
-
#
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
raise RuntimeError(
|
|
1039
|
-
f"SAFETY CHECK FAILED: Attempted to reset password on default branch "
|
|
1040
|
-
f"{branch.id}. This should never happen - the plugin creates new "
|
|
1041
|
-
f"branches and should never operate on the default branch. "
|
|
1042
|
-
f"Please report this bug at https://github.com/ZainRizvi/pytest-neon/issues"
|
|
1043
|
-
)
|
|
1044
|
-
|
|
1045
|
-
# Reset password to get the password value
|
|
1046
|
-
# (newly created branches don't expose password)
|
|
1047
|
-
# Wrap in retry logic to handle rate limits
|
|
1048
|
-
# See: https://api-docs.neon.tech/reference/api-rate-limiting
|
|
1049
|
-
password_response = _retry_on_rate_limit(
|
|
1050
|
-
lambda: neon.role_password_reset(
|
|
1081
|
+
# Get password using reveal (not reset) to avoid invalidating existing connections
|
|
1082
|
+
# See: https://api-docs.neon.tech/reference/getprojectbranchrolepassword
|
|
1083
|
+
password = _retry_on_rate_limit(
|
|
1084
|
+
lambda: _reveal_role_password(
|
|
1085
|
+
api_key=api_key,
|
|
1051
1086
|
project_id=project_id,
|
|
1052
1087
|
branch_id=branch.id,
|
|
1053
1088
|
role_name=role_name,
|
|
1054
1089
|
),
|
|
1055
|
-
operation_name="
|
|
1090
|
+
operation_name="role_password_reveal",
|
|
1056
1091
|
)
|
|
1057
|
-
password = password_response.role.password
|
|
1058
1092
|
|
|
1059
1093
|
# Build connection string
|
|
1060
1094
|
connection_string = (
|
|
@@ -1170,16 +1204,10 @@ def _create_readonly_endpoint(
|
|
|
1170
1204
|
|
|
1171
1205
|
host = endpoint.host
|
|
1172
1206
|
|
|
1173
|
-
#
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
branch_id=branch.branch_id,
|
|
1178
|
-
role_name=role_name,
|
|
1179
|
-
),
|
|
1180
|
-
operation_name="role_password_reset_readonly",
|
|
1181
|
-
)
|
|
1182
|
-
password = password_response.role.password
|
|
1207
|
+
# Reuse the password from the parent branch's connection string.
|
|
1208
|
+
# DO NOT call role_password_reset here - it would invalidate the
|
|
1209
|
+
# password used by the parent branch's read_write endpoint.
|
|
1210
|
+
password = _extract_password_from_connection_string(branch.connection_string)
|
|
1183
1211
|
|
|
1184
1212
|
# Build connection string for the read_only endpoint
|
|
1185
1213
|
connection_string = (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-neon
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pytest_neon/__init__.py,sha256=fFWOT5wYJg_WkV_FjkC8rv2YhvuyJAEHIGdsaG4c7iY,398
|
|
2
|
+
pytest_neon/plugin.py,sha256=Kokmw9qvfDiuvvqJMSx3mTJ7tKHQ4GouwvE9wvHbEZ4,76270
|
|
3
|
+
pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
pytest_neon-2.3.2.dist-info/METADATA,sha256=aY4AnACisvVnH2L1Raf5VzK4RNNMWwuU9ctmCZKI0ic,23149
|
|
5
|
+
pytest_neon-2.3.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
pytest_neon-2.3.2.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
|
|
7
|
+
pytest_neon-2.3.2.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
|
|
8
|
+
pytest_neon-2.3.2.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pytest_neon/__init__.py,sha256=FXdSRppBRXZYUknN1S7PKgX_m-3mNsqPn62ADFX65gg,398
|
|
2
|
-
pytest_neon/plugin.py,sha256=dRW-BIGDCevZ_CQcTwCREqcUNmbLMTD-qSxU99x9FAU,75414
|
|
3
|
-
pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
pytest_neon-2.3.0.dist-info/METADATA,sha256=EMgs_UAYwziYhbStM70pIswpsVynGF7QY9U-0OxHQFQ,23149
|
|
5
|
-
pytest_neon-2.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
-
pytest_neon-2.3.0.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
|
|
7
|
-
pytest_neon-2.3.0.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
|
|
8
|
-
pytest_neon-2.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|