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 CHANGED
@@ -9,7 +9,7 @@ from pytest_neon.plugin import (
9
9
  neon_engine,
10
10
  )
11
11
 
12
- __version__ = "2.3.0"
12
+ __version__ = "2.3.2"
13
13
  __all__ = [
14
14
  "NeonBranch",
15
15
  "neon_branch",
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._reset_password_and_build_connection_string(
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
- # Get password for the read_only endpoint
541
- connection_string = self._reset_password_and_build_connection_string(
542
- branch.branch_id, host
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 _reset_password_and_build_connection_string(
666
+ def _get_password_and_build_connection_string(
619
667
  self, branch_id: str, host: str
620
668
  ) -> str:
621
- """Reset role password and build connection string."""
622
- password_response = _retry_on_rate_limit(
623
- lambda: self._neon.role_password_reset(
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="role_password_reset",
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
- # SAFETY CHECK: Ensure we never reset password on the default/production branch
1034
- # This should be impossible since we just created this branch, but we check
1035
- # defensively to prevent catastrophic mistakes if there's ever a bug
1036
- default_branch_id = getattr(config, "_neon_default_branch_id", None)
1037
- if default_branch_id and branch.id == default_branch_id:
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="role_password_reset",
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
- # Reset password to get the password value for this endpoint
1174
- password_response = _retry_on_rate_limit(
1175
- lambda: neon.role_password_reset(
1176
- project_id=branch.project_id,
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.0
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,,