pytest-neon 0.2.1__py3-none-any.whl → 0.4.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 CHANGED
@@ -9,7 +9,7 @@ from pytest_neon.plugin import (
9
9
  neon_engine,
10
10
  )
11
11
 
12
- __version__ = "0.2.1"
12
+ __version__ = "0.4.0"
13
13
  __all__ = [
14
14
  "NeonBranch",
15
15
  "neon_branch",
pytest_neon/plugin.py CHANGED
@@ -7,14 +7,12 @@ import time
7
7
  from collections.abc import Generator
8
8
  from dataclasses import dataclass
9
9
  from datetime import datetime, timedelta, timezone
10
- from typing import TYPE_CHECKING, Any
10
+ from typing import Any
11
11
 
12
12
  import pytest
13
13
  import requests
14
14
  from neon_api import NeonAPI
15
-
16
- if TYPE_CHECKING:
17
- pass
15
+ from neon_api.schema import EndpointState
18
16
 
19
17
  # Default branch expiry in seconds (10 minutes)
20
18
  DEFAULT_BRANCH_EXPIRY_SECONDS = 600
@@ -32,9 +30,10 @@ class NeonBranch:
32
30
 
33
31
 
34
32
  def pytest_addoption(parser: pytest.Parser) -> None:
35
- """Add Neon-specific command line options."""
33
+ """Add Neon-specific command line options and ini settings."""
36
34
  group = parser.getgroup("neon", "Neon database branching")
37
35
 
36
+ # CLI options
38
37
  group.addoption(
39
38
  "--neon-api-key",
40
39
  dest="neon_api_key",
@@ -53,13 +52,11 @@ def pytest_addoption(parser: pytest.Parser) -> None:
53
52
  group.addoption(
54
53
  "--neon-database",
55
54
  dest="neon_database",
56
- default="neondb",
57
55
  help="Database name (default: neondb)",
58
56
  )
59
57
  group.addoption(
60
58
  "--neon-role",
61
59
  dest="neon_role",
62
- default="neondb_owner",
63
60
  help="Database role (default: neondb_owner)",
64
61
  )
65
62
  group.addoption(
@@ -72,7 +69,6 @@ def pytest_addoption(parser: pytest.Parser) -> None:
72
69
  "--neon-branch-expiry",
73
70
  dest="neon_branch_expiry",
74
71
  type=int,
75
- default=DEFAULT_BRANCH_EXPIRY_SECONDS,
76
72
  help=(
77
73
  f"Branch auto-expiry in seconds "
78
74
  f"(default: {DEFAULT_BRANCH_EXPIRY_SECONDS}). Set to 0 to disable."
@@ -81,19 +77,62 @@ def pytest_addoption(parser: pytest.Parser) -> None:
81
77
  group.addoption(
82
78
  "--neon-env-var",
83
79
  dest="neon_env_var",
84
- default="DATABASE_URL",
85
80
  help="Environment variable to set with connection string (default: DATABASE_URL)", # noqa: E501
86
81
  )
87
82
 
83
+ # INI file settings (pytest.ini, pyproject.toml, etc.)
84
+ parser.addini("neon_api_key", "Neon API key", default=None)
85
+ parser.addini("neon_project_id", "Neon project ID", default=None)
86
+ parser.addini("neon_parent_branch", "Parent branch ID", default=None)
87
+ parser.addini("neon_database", "Database name", default="neondb")
88
+ parser.addini("neon_role", "Database role", default="neondb_owner")
89
+ parser.addini(
90
+ "neon_keep_branches",
91
+ "Don't delete branches after tests",
92
+ type="bool",
93
+ default=False,
94
+ )
95
+ parser.addini(
96
+ "neon_branch_expiry",
97
+ "Branch auto-expiry in seconds",
98
+ default=str(DEFAULT_BRANCH_EXPIRY_SECONDS),
99
+ )
100
+ parser.addini(
101
+ "neon_env_var",
102
+ "Environment variable for connection string",
103
+ default="DATABASE_URL",
104
+ )
105
+
88
106
 
89
107
  def _get_config_value(
90
- config: pytest.Config, option: str, env_var: str, default: str | None = None
108
+ config: pytest.Config,
109
+ option: str,
110
+ env_var: str,
111
+ ini_name: str | None = None,
112
+ default: str | None = None,
91
113
  ) -> str | None:
92
- """Get config value from CLI option, env var, or default."""
114
+ """Get config value from CLI option, env var, ini setting, or default.
115
+
116
+ Priority order: CLI option > environment variable > ini setting > default
117
+ """
118
+ # 1. CLI option (highest priority)
93
119
  value = config.getoption(option, default=None)
94
120
  if value is not None:
95
121
  return value
96
- return os.environ.get(env_var, default)
122
+
123
+ # 2. Environment variable
124
+ env_value = os.environ.get(env_var)
125
+ if env_value is not None:
126
+ return env_value
127
+
128
+ # 3. INI setting (pytest.ini, pyproject.toml, etc.)
129
+ if ini_name is not None:
130
+ ini_value = config.getini(ini_name)
131
+ if ini_value:
132
+ return ini_value
133
+
134
+ # 4. Default
135
+ return default
97
136
 
98
137
 
99
138
  def _create_neon_branch(
@@ -106,20 +145,32 @@ def _create_neon_branch(
106
145
  """
107
146
  config = request.config
108
147
 
109
- api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY")
110
- project_id = _get_config_value(config, "neon_project_id", "NEON_PROJECT_ID")
148
+ api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY", "neon_api_key")
149
+ project_id = _get_config_value(
150
+ config, "neon_project_id", "NEON_PROJECT_ID", "neon_project_id"
151
+ )
111
152
  parent_branch_id = _get_config_value(
112
- config, "neon_parent_branch", "NEON_PARENT_BRANCH_ID"
153
+ config, "neon_parent_branch", "NEON_PARENT_BRANCH_ID", "neon_parent_branch"
113
154
  )
114
155
  database_name = _get_config_value(
115
- config, "neon_database", "NEON_DATABASE", "neondb"
156
+ config, "neon_database", "NEON_DATABASE", "neon_database", "neondb"
157
+ )
158
+ role_name = _get_config_value(
159
+ config, "neon_role", "NEON_ROLE", "neon_role", "neondb_owner"
116
160
  )
117
- role_name = _get_config_value(config, "neon_role", "NEON_ROLE", "neondb_owner")
118
- keep_branches = config.getoption("neon_keep_branches", default=False)
119
- branch_expiry = config.getoption(
120
- "neon_branch_expiry", default=DEFAULT_BRANCH_EXPIRY_SECONDS
161
+
162
+ # For boolean/int options, check CLI first, then ini
163
+ keep_branches = config.getoption("neon_keep_branches", default=None)
164
+ if keep_branches is None:
165
+ keep_branches = config.getini("neon_keep_branches")
166
+
167
+ branch_expiry = config.getoption("neon_branch_expiry", default=None)
168
+ if branch_expiry is None:
169
+ branch_expiry = int(config.getini("neon_branch_expiry"))
170
+
171
+ env_var_name = _get_config_value(
172
+ config, "neon_env_var", "", "neon_env_var", "DATABASE_URL"
121
173
  )
122
- env_var_name = config.getoption("neon_env_var", default="DATABASE_URL")
123
174
 
124
175
  if not api_key:
125
176
  pytest.skip(
@@ -168,9 +219,10 @@ def _create_neon_branch(
168
219
  raise RuntimeError(f"No endpoint created for branch {branch.id}")
169
220
 
170
221
  # Wait for endpoint to be ready (it starts in "init" state)
171
- # Endpoints typically become active in 1-2 seconds
222
+ # Endpoints typically become active in 1-2 seconds, but we allow up to 60s
223
+ # to handle occasional Neon API slowness or high load scenarios
172
224
  max_wait_seconds = 60
173
- poll_interval = 0.5
225
+ poll_interval = 0.5 # Poll every 500ms for responsive feedback
174
226
  waited = 0.0
175
227
 
176
228
  while True:
@@ -180,7 +232,7 @@ def _create_neon_branch(
180
232
  endpoint = endpoint_response.endpoint
181
233
  state = endpoint.current_state
182
234
 
183
- if state == "active" or str(state) == "EndpointState.active":
235
+ if state == EndpointState.active:
184
236
  break
185
237
 
186
238
  if waited >= max_wait_seconds:
@@ -254,7 +306,7 @@ def _reset_branch_to_parent(branch: NeonBranch, api_key: str) -> None:
254
306
  "Content-Type": "application/json",
255
307
  }
256
308
  response = requests.post(
257
- url, headers=headers, json={"source_branch_id": branch.parent_id}
309
+ url, headers=headers, json={"source_branch_id": branch.parent_id}, timeout=30
258
310
  )
259
311
  response.raise_for_status()
260
312
 
@@ -302,7 +354,16 @@ def neon_branch(
302
354
  conn_string = neon_branch.connection_string
303
355
  """
304
356
  config = request.config
305
- api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY")
357
+ api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY", "neon_api_key")
358
+
359
+ # Validate that branch has a parent for reset functionality
360
+ if not _neon_branch_for_reset.parent_id:
361
+ pytest.fail(
362
+ f"\n\nBranch {_neon_branch_for_reset.branch_id} has no parent. "
363
+ f"The neon_branch fixture requires a parent branch for reset.\n\n"
364
+ f"Use neon_branch_shared if you don't need reset, or specify "
365
+ f"a parent branch with --neon-parent-branch or NEON_PARENT_BRANCH_ID."
366
+ )
306
367
 
307
368
  yield _neon_branch_for_reset
308
369
 
@@ -311,11 +372,11 @@ def neon_branch(
311
372
  try:
312
373
  _reset_branch_to_parent(branch=_neon_branch_for_reset, api_key=api_key)
313
374
  except Exception as e:
314
- import warnings
315
-
316
- warnings.warn(
317
- f"Failed to reset branch {_neon_branch_for_reset.branch_id}: {e}",
318
- stacklevel=2,
375
+ pytest.fail(
376
+ f"\n\nFailed to reset branch {_neon_branch_for_reset.branch_id} "
377
+ f"after test. Subsequent tests in this module may see dirty "
378
+ f"database state.\n\nError: {e}\n\n"
379
+ f"To keep the branch for debugging, use --neon-keep-branches"
319
380
  )
320
381
 
321
382
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-neon
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: Pytest plugin for Neon database branch isolation in tests
5
5
  Project-URL: Homepage, https://github.com/zain/pytest-neon
6
6
  Project-URL: Repository, https://github.com/zain/pytest-neon
@@ -120,6 +120,7 @@ Returns a `NeonBranch` dataclass with:
120
120
  - `project_id`: The Neon project ID
121
121
  - `connection_string`: Full PostgreSQL connection URI
122
122
  - `host`: The database host
123
+ - `parent_id`: The parent branch ID (used for resets)
123
124
 
124
125
  ```python
125
126
  import os
@@ -237,6 +238,30 @@ pytest --neon-branch-expiry=0
237
238
  pytest --neon-env-var=TEST_DATABASE_URL
238
239
  ```
239
240
 
241
+ ### pyproject.toml / pytest.ini
242
+
243
+ You can also configure options in your `pyproject.toml`:
244
+
245
+ ```toml
246
+ [tool.pytest.ini_options]
247
+ neon_database = "mydb"
248
+ neon_role = "myrole"
249
+ neon_keep_branches = true
250
+ neon_branch_expiry = "300"
251
+ ```
252
+
253
+ Or in `pytest.ini`:
254
+
255
+ ```ini
256
+ [pytest]
257
+ neon_database = mydb
258
+ neon_role = myrole
259
+ neon_keep_branches = true
260
+ neon_branch_expiry = 300
261
+ ```
262
+
263
+ **Priority order**: CLI options > environment variables > ini settings > defaults
264
+
240
265
  ## CI/CD Integration
241
266
 
242
267
  ### GitHub Actions
@@ -276,6 +301,28 @@ jobs:
276
301
 
277
302
  Branches use copy-on-write storage, so you only pay for data that differs from the parent branch.
278
303
 
304
+ ### What Reset Does
305
+
306
+ The `neon_branch` fixture uses Neon's branch restore API to reset database state after each test:
307
+
308
+ - **Data changes are reverted**: All INSERT, UPDATE, DELETE operations are undone
309
+ - **Schema changes are reverted**: CREATE TABLE, ALTER TABLE, DROP TABLE, etc. are undone
310
+ - **Sequences are reset**: Auto-increment counters return to parent state
311
+ - **Complete rollback**: The branch is restored to the exact state of the parent at the time the child branch was created
312
+
313
+ This is similar to database transactions but at the branch level.
314
+
315
+ ## Limitations
316
+
317
+ ### Parallel Test Execution
318
+
319
+ This plugin sets the `DATABASE_URL` environment variable, which is process-global. This means it is **not compatible with pytest-xdist** or other parallel test runners that run tests in the same process.
320
+
321
+ If you need parallel execution, you can:
322
+ - Use `neon_branch.connection_string` directly instead of relying on `DATABASE_URL`
323
+ - Run with `pytest-xdist --dist=loadfile` to keep modules in separate processes
324
+ - Run tests serially (default pytest behavior)
325
+
279
326
  ## Troubleshooting
280
327
 
281
328
  ### "psycopg not installed" or "psycopg2 not installed"
@@ -0,0 +1,8 @@
1
+ pytest_neon/__init__.py,sha256=C_3fxrYTEBtLi_n9gJNJ9KyDk3yPFYcAmVDm87Sj-JQ,398
2
+ pytest_neon/plugin.py,sha256=dYNcaC1kiBWsoi-_jgANTQsCRRyi3ynONqBG0ZXX4i8,19526
3
+ pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pytest_neon-0.4.0.dist-info/METADATA,sha256=XIRZcTXGiYefebFw1Vm6Uc2cL9ZvHTxzukWtUmtN40Q,11052
5
+ pytest_neon-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ pytest_neon-0.4.0.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
7
+ pytest_neon-0.4.0.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
8
+ pytest_neon-0.4.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pytest_neon/__init__.py,sha256=tFnPqmxYhQPW1IlP53MTxeCumOOvS-OCuuMUn0hW7Zg,398
2
- pytest_neon/plugin.py,sha256=-7NHdQ-nqfR-ZQXFmLBu_vV445LTLJ44UXnkQPtCgOQ,17217
3
- pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- pytest_neon-0.2.1.dist-info/METADATA,sha256=sNq5SF3HGzgCzh9Y1o9cGLSbPTcF8BV1kegvEdMeVt0,9496
5
- pytest_neon-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
- pytest_neon-0.2.1.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
7
- pytest_neon-0.2.1.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
8
- pytest_neon-0.2.1.dist-info/RECORD,,