rrq 0.3.5__tar.gz → 0.3.6__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rrq
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: RRQ is a Python library for creating reliable job queues using Redis and asyncio
5
5
  Project-URL: Homepage, https://github.com/getresq/rrq
6
6
  Project-URL: Bug Tracker, https://github.com/getresq/rrq/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rrq"
7
- version = "0.3.5"
7
+ version = "0.3.6"
8
8
  authors = [
9
9
  { name = "Mazdak Rezvani", email = "mazdak@me.com" },
10
10
  ]
@@ -18,6 +18,14 @@ from .settings import RRQSettings
18
18
  from .store import JobStore
19
19
  from .worker import RRQWorker
20
20
 
21
+ # Attempt to import dotenv components for .env file loading
22
+ try:
23
+ from dotenv import find_dotenv, load_dotenv
24
+
25
+ DOTENV_AVAILABLE = True
26
+ except ImportError:
27
+ DOTENV_AVAILABLE = False
28
+
21
29
  logger = logging.getLogger(__name__)
22
30
 
23
31
 
@@ -28,12 +36,21 @@ def _load_app_settings(settings_object_path: str | None = None) -> RRQSettings:
28
36
  If the environment variable is not set, will create a default settings object.
29
37
  RRQ Setting objects, automatically pick up ENVIRONMENT variables starting with RRQ_.
30
38
 
39
+ This function will also attempt to load a .env file if python-dotenv is installed
40
+ and a .env file is found. System environment variables take precedence over .env variables.
41
+
31
42
  Args:
32
43
  settings_object_path: A string representing the path to the settings object. (e.g. "myapp.worker_config.rrq_settings").
33
44
 
34
45
  Returns:
35
46
  The RRQSettings object.
36
47
  """
48
+ if DOTENV_AVAILABLE:
49
+ dotenv_path = find_dotenv(usecwd=True)
50
+ if dotenv_path:
51
+ logger.debug(f"Loading .env file at: {dotenv_path}...")
52
+ load_dotenv(dotenv_path=dotenv_path, override=False)
53
+
37
54
  try:
38
55
  if settings_object_path is None:
39
56
  settings_object_path = os.getenv("RRQ_SETTINGS")
@@ -153,12 +170,10 @@ def start_rrq_worker_subprocess(
153
170
  ) -> subprocess.Popen | None:
154
171
  """Start an RRQ worker process, optionally for specific queues."""
155
172
  command = ["rrq", "worker", "run"]
173
+
156
174
  if settings_object_path:
157
175
  command.extend(["--settings", settings_object_path])
158
- else:
159
- raise ValueError(
160
- "start_rrq_worker_subprocess called without settings_object_path!"
161
- )
176
+
162
177
  # Add queue filters if specified
163
178
  if queues:
164
179
  for q in queues:
@@ -220,16 +235,6 @@ async def watch_rrq_worker_impl(
220
235
  settings_object_path: str | None = None,
221
236
  queues: list[str] | None = None,
222
237
  ) -> None:
223
- if not settings_object_path:
224
- click.echo(
225
- click.style(
226
- "ERROR: 'rrq worker watch' requires --settings to be specified.",
227
- fg="red",
228
- ),
229
- err=True,
230
- )
231
- sys.exit(1)
232
-
233
238
  abs_watch_path = os.path.abspath(watch_path)
234
239
  click.echo(
235
240
  f"Watching for file changes in {abs_watch_path} to restart RRQ worker (app settings: {settings_object_path})..."
@@ -295,7 +300,7 @@ def rrq():
295
300
  """RRQ: Reliable Redis Queue Command Line Interface.
296
301
 
297
302
  Provides tools for running RRQ workers, checking system health,
298
- and managing jobs. Requires an application-specific --settings module
303
+ and managing jobs. Requires an application-specific settings object
299
304
  for most operations.
300
305
  """
301
306
  pass
@@ -329,6 +334,7 @@ def worker_cli():
329
334
  help=(
330
335
  "Python settings path for application worker settings "
331
336
  "(e.g., myapp.worker_config.rrq_settings). "
337
+ "Alternatively, this can be specified as RRQ_SETTINGS env variable. "
332
338
  "The specified settings object must include a `job_registry: JobRegistry`."
333
339
  ),
334
340
  )
@@ -337,7 +343,9 @@ def worker_run_command(
337
343
  queues: tuple[str, ...],
338
344
  settings_object_path: str,
339
345
  ):
340
- """Run an RRQ worker process. Requires --settings."""
346
+ """Run an RRQ worker process.
347
+ Requires an application-specific settings object.
348
+ """
341
349
  rrq_settings = _load_app_settings(settings_object_path)
342
350
 
343
351
  # Determine queues to poll
@@ -418,7 +426,9 @@ def worker_watch_command(
418
426
  settings_object_path: str,
419
427
  queues: tuple[str, ...],
420
428
  ):
421
- """Run the RRQ worker with auto-restart on file changes in PATH. Requires --settings."""
429
+ """Run the RRQ worker with auto-restart on file changes in PATH.
430
+ Requires an application-specific settings object.
431
+ """
422
432
  # Run watch with optional queue filters
423
433
  asyncio.run(
424
434
  watch_rrq_worker_impl(
@@ -446,7 +456,9 @@ def worker_watch_command(
446
456
  ),
447
457
  )
448
458
  def check_command(settings_object_path: str):
449
- """Perform a health check on active RRQ worker(s). Requires --settings."""
459
+ """Perform a health check on active RRQ worker(s).
460
+ Requires an application-specific settings object.
461
+ """
450
462
  click.echo("Performing RRQ health check...")
451
463
  healthy = asyncio.run(
452
464
  check_health_async_impl(settings_object_path=settings_object_path)
@@ -271,11 +271,21 @@ def test_worker_watch_command_with_queues(
271
271
  assert kwargs.get("queues") == ["alpha", "beta"]
272
272
 
273
273
 
274
- def test_worker_watch_command_missing_settings(cli_runner):
275
- """Test 'rrq worker watch' without --settings."""
274
+ @mock.patch("rrq.cli.watch_rrq_worker_impl")
275
+ def test_worker_watch_command_missing_settings(mock_watch_impl, cli_runner):
276
+ """Test 'rrq worker watch' without --settings uses default settings."""
277
+ async def dummy_watch_impl(path, settings_object_path=None, queues=None):
278
+ pass
279
+
280
+ mock_watch_impl.side_effect = dummy_watch_impl
281
+
276
282
  result = cli_runner.invoke(cli.rrq, ["worker", "watch", "--path", "."])
277
- assert result.exit_code != 0
278
- assert "requires --settings to be specified" in result.output
283
+ assert result.exit_code == 0
284
+ mock_watch_impl.assert_called_once()
285
+ args, kwargs = mock_watch_impl.call_args
286
+ assert args[0] == "." # Path argument
287
+ assert kwargs.get("settings_object_path") is None
288
+ assert kwargs.get("queues") is None
279
289
 
280
290
 
281
291
  def test_worker_watch_command_invalid_path(cli_runner, mock_app_settings_path):
@@ -437,6 +447,63 @@ def test_load_app_settings_default(tmp_path, monkeypatch):
437
447
  settings = _load_app_settings(None)
438
448
  assert isinstance(settings, RRQSettings)
439
449
 
450
+ def test_load_app_settings_from_env_var(tmp_path, monkeypatch):
451
+ """Test loading settings via RRQ_SETTINGS environment variable."""
452
+ # Create a fake module with a settings instance
453
+ module_dir = tmp_path / "env_mod"
454
+ module_dir.mkdir()
455
+ settings_file = module_dir / "settings_module.py"
456
+ settings_file.write_text(
457
+ """
458
+ from rrq.settings import RRQSettings
459
+ from rrq.registry import JobRegistry
460
+
461
+ test_env_registry = JobRegistry()
462
+ test_env_settings = RRQSettings(redis_dsn="redis://envvar:333/7", job_registry=test_env_registry)
463
+ """
464
+ )
465
+ # Ensure the new module path is discoverable
466
+ monkeypatch.syspath_prepend(str(tmp_path))
467
+ # Set the environment variable to point to our settings object
468
+ monkeypatch.setenv("RRQ_SETTINGS", "env_mod.settings_module.test_env_settings")
469
+ # Load settings without explicit argument
470
+ settings_object = _load_app_settings(None)
471
+ # Import the module to get the original instance for identity check
472
+ import importlib
473
+ imported_module = importlib.import_module("env_mod.settings_module")
474
+ assert settings_object is getattr(imported_module, "test_env_settings")
475
+
476
+ @pytest.mark.skipif(not cli.DOTENV_AVAILABLE, reason="python-dotenv not available")
477
+ def test_load_app_settings_from_dotenv(tmp_path, monkeypatch):
478
+ """Test loading settings values from a .env file."""
479
+ # Ensure no pre-existing env var for redis_dsn or settings
480
+ monkeypatch.delenv("RRQ_REDIS_DSN", raising=False)
481
+ monkeypatch.delenv("RRQ_SETTINGS", raising=False)
482
+ # Create a .env file with a custom Redis DSN
483
+ env_file = tmp_path / ".env"
484
+ env_file.write_text("RRQ_REDIS_DSN=redis://dotenv:2222/2")
485
+ # Change CWD so find_dotenv will locate the .env file
486
+ monkeypatch.chdir(tmp_path)
487
+ # Load settings without explicit argument
488
+ settings_object = _load_app_settings(None)
489
+ # The redis_dsn should reflect the value from .env
490
+ assert settings_object.redis_dsn == "redis://dotenv:2222/2"
491
+
492
+ @pytest.mark.skipif(not cli.DOTENV_AVAILABLE, reason="python-dotenv not available")
493
+ def test_load_app_settings_dotenv_not_override_system_env(tmp_path, monkeypatch):
494
+ """System environment variables should override .env file values."""
495
+ # Set system env var for redis_dsn
496
+ monkeypatch.setenv("RRQ_REDIS_DSN", "redis://env:1111/1")
497
+ monkeypatch.delenv("RRQ_SETTINGS", raising=False)
498
+ # Create a .env file with a different Redis DSN
499
+ env_file = tmp_path / ".env"
500
+ env_file.write_text("RRQ_REDIS_DSN=redis://dotenv:2222/2")
501
+ monkeypatch.chdir(tmp_path)
502
+ # Load settings without explicit argument
503
+ settings_object = _load_app_settings(None)
504
+ # Should use system env var, not .env value
505
+ assert settings_object.redis_dsn == "redis://env:1111/1"
506
+
440
507
 
441
508
  def test_load_app_settings_invalid(monkeypatch, capsys):
442
509
  # Invalid import path should exit with code 1
@@ -448,8 +515,8 @@ def test_load_app_settings_invalid(monkeypatch, capsys):
448
515
 
449
516
 
450
517
  def test_start_rrq_worker_subprocess_no_settings():
451
- # Calling without settings should raise
452
- with pytest.raises(ValueError):
518
+ # Calling without settings should attempt to start default worker process and fail if 'rrq' is not in PATH
519
+ with pytest.raises(FileNotFoundError):
453
520
  start_rrq_worker_subprocess()
454
521
 
455
522
 
@@ -370,7 +370,7 @@ hiredis = [
370
370
 
371
371
  [[package]]
372
372
  name = "rrq"
373
- version = "0.3.0"
373
+ version = "0.3.5"
374
374
  source = { editable = "." }
375
375
  dependencies = [
376
376
  { name = "click" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes