devservices 1.1.5__tar.gz → 1.1.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.
Files changed (66) hide show
  1. {devservices-1.1.5 → devservices-1.1.6}/PKG-INFO +1 -1
  2. {devservices-1.1.5 → devservices-1.1.6}/README.md +1 -1
  3. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/up.py +2 -1
  4. {devservices-1.1.5 → devservices-1.1.6}/devservices/main.py +0 -1
  5. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/docker_compose.py +49 -11
  6. {devservices-1.1.5 → devservices-1.1.6}/devservices.egg-info/PKG-INFO +1 -1
  7. {devservices-1.1.5 → devservices-1.1.6}/pyproject.toml +1 -1
  8. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_up.py +215 -0
  9. {devservices-1.1.5 → devservices-1.1.6}/LICENSE.md +0 -0
  10. {devservices-1.1.5 → devservices-1.1.6}/devservices/__init__.py +0 -0
  11. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/__init__.py +0 -0
  12. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/down.py +0 -0
  13. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/list_dependencies.py +0 -0
  14. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/list_services.py +0 -0
  15. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/logs.py +0 -0
  16. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/purge.py +0 -0
  17. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/reset.py +0 -0
  18. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/serve.py +0 -0
  19. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/status.py +0 -0
  20. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/toggle.py +0 -0
  21. {devservices-1.1.5 → devservices-1.1.6}/devservices/commands/update.py +0 -0
  22. {devservices-1.1.5 → devservices-1.1.6}/devservices/configs/service_config.py +0 -0
  23. {devservices-1.1.5 → devservices-1.1.6}/devservices/constants.py +0 -0
  24. {devservices-1.1.5 → devservices-1.1.6}/devservices/exceptions.py +0 -0
  25. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/__init__.py +0 -0
  26. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/check_for_update.py +0 -0
  27. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/console.py +0 -0
  28. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/dependencies.py +0 -0
  29. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/devenv.py +0 -0
  30. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/docker.py +0 -0
  31. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/file_lock.py +0 -0
  32. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/git.py +0 -0
  33. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/install_binary.py +0 -0
  34. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/services.py +0 -0
  35. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/state.py +0 -0
  36. {devservices-1.1.5 → devservices-1.1.6}/devservices/utils/supervisor.py +0 -0
  37. {devservices-1.1.5 → devservices-1.1.6}/devservices.egg-info/SOURCES.txt +0 -0
  38. {devservices-1.1.5 → devservices-1.1.6}/devservices.egg-info/dependency_links.txt +0 -0
  39. {devservices-1.1.5 → devservices-1.1.6}/devservices.egg-info/entry_points.txt +0 -0
  40. {devservices-1.1.5 → devservices-1.1.6}/devservices.egg-info/requires.txt +0 -0
  41. {devservices-1.1.5 → devservices-1.1.6}/devservices.egg-info/top_level.txt +0 -0
  42. {devservices-1.1.5 → devservices-1.1.6}/setup.cfg +0 -0
  43. {devservices-1.1.5 → devservices-1.1.6}/testing/__init__.py +0 -0
  44. {devservices-1.1.5 → devservices-1.1.6}/testing/utils.py +0 -0
  45. {devservices-1.1.5 → devservices-1.1.6}/tests/__init__.py +0 -0
  46. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_down.py +0 -0
  47. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_list_dependencies.py +0 -0
  48. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_list_services.py +0 -0
  49. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_logs.py +0 -0
  50. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_purge.py +0 -0
  51. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_reset.py +0 -0
  52. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_serve.py +0 -0
  53. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_status.py +0 -0
  54. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_toggle.py +0 -0
  55. {devservices-1.1.5 → devservices-1.1.6}/tests/commands/test_update.py +0 -0
  56. {devservices-1.1.5 → devservices-1.1.6}/tests/configs/test_service_config.py +0 -0
  57. {devservices-1.1.5 → devservices-1.1.6}/tests/conftest.py +0 -0
  58. {devservices-1.1.5 → devservices-1.1.6}/tests/utils/test_check_for_update.py +0 -0
  59. {devservices-1.1.5 → devservices-1.1.6}/tests/utils/test_dependencies.py +0 -0
  60. {devservices-1.1.5 → devservices-1.1.6}/tests/utils/test_docker.py +0 -0
  61. {devservices-1.1.5 → devservices-1.1.6}/tests/utils/test_docker_compose.py +0 -0
  62. {devservices-1.1.5 → devservices-1.1.6}/tests/utils/test_git.py +0 -0
  63. {devservices-1.1.5 → devservices-1.1.6}/tests/utils/test_install_binary.py +0 -0
  64. {devservices-1.1.5 → devservices-1.1.6}/tests/utils/test_services.py +0 -0
  65. {devservices-1.1.5 → devservices-1.1.6}/tests/utils/test_state.py +0 -0
  66. {devservices-1.1.5 → devservices-1.1.6}/tests/utils/test_supervisor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.1.5
3
+ Version: 1.1.6
4
4
  Requires-Python: >=3.11
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -31,7 +31,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
31
31
  The recommended way to install devservices is through a virtualenv in the requirements.txt. Once that is installed and a devservices config file is added, you should be able to run `devservices up` to begin local development.
32
32
 
33
33
  ```
34
- devservices==1.1.5
34
+ devservices==1.1.6
35
35
  ```
36
36
 
37
37
  ### 2. Add devservices config files
@@ -182,7 +182,7 @@ def up(args: Namespace, existing_status: Status | None = None) -> None:
182
182
  def _pull_dependency_images(
183
183
  cmd: DockerComposeCommand, current_env: dict[str, str], status: Status
184
184
  ) -> None:
185
- run_cmd(cmd.full_command, current_env)
185
+ run_cmd(cmd.full_command, current_env, retries=4)
186
186
  for dependency in cmd.services:
187
187
  status.info(f"Pulled image for {dependency}")
188
188
 
@@ -224,6 +224,7 @@ def _up(
224
224
  )
225
225
  ),
226
226
  )
227
+
227
228
  # Pull all images in parallel
228
229
  status.info("Pulling images")
229
230
  pull_commands = get_docker_compose_commands_to_run(
@@ -78,7 +78,6 @@ if not disable_sentry:
78
78
  dsn="https://56470da7302c16e83141f62f88e46449@o1.ingest.us.sentry.io/4507946704961536",
79
79
  traces_sample_rate=1.0,
80
80
  profiles_sample_rate=1.0,
81
- enable_tracing=True,
82
81
  integrations=[ArgvIntegration()],
83
82
  environment=sentry_environment,
84
83
  before_send=before_send_error,
@@ -5,7 +5,9 @@ import logging
5
5
  import os
6
6
  import platform
7
7
  import re
8
+ import shlex
8
9
  import subprocess
10
+ import time
9
11
  from typing import cast
10
12
  from typing import NamedTuple
11
13
 
@@ -290,15 +292,51 @@ def get_docker_compose_commands_to_run(
290
292
  return docker_compose_commands
291
293
 
292
294
 
293
- def run_cmd(cmd: list[str], env: dict[str, str]) -> subprocess.CompletedProcess[str]:
295
+ def run_cmd(
296
+ cmd: list[str],
297
+ env: dict[str, str],
298
+ retries: int = 0,
299
+ retry_initial_wait: float = 5.0,
300
+ retry_exp: float = 2.0,
301
+ ) -> subprocess.CompletedProcess[str]:
302
+ if retries < 0:
303
+ raise ValueError("Retries cannot be negative")
304
+
294
305
  logger = logging.getLogger(LOGGER_NAME)
295
- try:
296
- logger.debug("Running command: %s", " ".join(cmd))
297
- return subprocess.run(cmd, check=True, capture_output=True, text=True, env=env)
298
- except subprocess.CalledProcessError as e:
299
- raise DockerComposeError(
300
- command=" ".join(cmd),
301
- returncode=e.returncode,
302
- stdout=e.stdout,
303
- stderr=e.stderr,
304
- ) from e
306
+ console = Console()
307
+ cmd_pretty = shlex.join(cmd)
308
+
309
+ proc = None
310
+ retries += 1 # initial try
311
+
312
+ while retries > 0:
313
+ retries -= 1
314
+ try:
315
+ logger.debug(f"Running command: {cmd_pretty}")
316
+ proc = subprocess.run(
317
+ cmd, check=True, capture_output=True, text=True, env=env
318
+ )
319
+ return proc
320
+ except subprocess.CalledProcessError as e:
321
+ err = DockerComposeError(
322
+ command=cmd_pretty,
323
+ returncode=e.returncode,
324
+ stdout=e.stdout,
325
+ stderr=e.stderr,
326
+ )
327
+ if retries == 0:
328
+ raise err
329
+
330
+ console.warning(
331
+ f"""
332
+ Error: {err}
333
+
334
+ Retrying in {retry_initial_wait}s ({retries} retries left)...
335
+ """
336
+ )
337
+ time.sleep(retry_initial_wait)
338
+ retry_initial_wait *= retry_exp
339
+
340
+ # make mypy happy
341
+ assert proc is not None
342
+ return proc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.1.5
3
+ Version: 1.1.6
4
4
  Requires-Python: >=3.11
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devservices"
7
- version = "1.1.5"
7
+ version = "1.1.6"
8
8
  # 3.11 is just for internal pypi compat
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -5,6 +5,7 @@ import subprocess
5
5
  from argparse import Namespace
6
6
  from pathlib import Path
7
7
  from unittest import mock
8
+ from unittest.mock import call
8
9
 
9
10
  import pytest
10
11
 
@@ -345,6 +346,220 @@ def test_up_error(
345
346
  assert "Starting redis" not in captured.out.strip()
346
347
 
347
348
 
349
+ @mock.patch("time.sleep")
350
+ @mock.patch("devservices.utils.state.State.remove_service_entry")
351
+ @mock.patch("devservices.utils.state.State.update_service_entry")
352
+ @mock.patch("devservices.commands.up._create_devservices_network")
353
+ @mock.patch("devservices.commands.up.check_all_containers_healthy")
354
+ @mock.patch(
355
+ "devservices.utils.docker_compose.get_non_remote_services",
356
+ return_value={"clickhouse", "redis"},
357
+ )
358
+ def test_up_pull_error_timeout(
359
+ mock_get_non_remote_services: mock.Mock,
360
+ mock_check_all_containers_healthy: mock.Mock,
361
+ mock_create_devservices_network: mock.Mock,
362
+ mock_update_service_entry: mock.Mock,
363
+ mock_remove_service_entry: mock.Mock,
364
+ mock_sleep: mock.Mock,
365
+ capsys: pytest.CaptureFixture[str],
366
+ tmp_path: Path,
367
+ ) -> None:
368
+ config = {
369
+ "x-sentry-service-config": {
370
+ "version": 0.1,
371
+ "service_name": "example-service",
372
+ "dependencies": {
373
+ "redis": {"description": "Redis"},
374
+ "clickhouse": {"description": "Clickhouse"},
375
+ },
376
+ "modes": {"default": ["redis", "clickhouse"]},
377
+ },
378
+ "services": {
379
+ "redis": {"image": "redis:6.2.14-alpine"},
380
+ "clickhouse": {
381
+ "image": "altinity/clickhouse-server:23.8.11.29.altinitystable"
382
+ },
383
+ },
384
+ }
385
+
386
+ create_config_file(tmp_path, config)
387
+ os.chdir(tmp_path)
388
+
389
+ args = Namespace(service_name=None, debug=False, mode="default")
390
+
391
+ with pytest.raises(SystemExit):
392
+ with mock.patch(
393
+ "devservices.utils.docker_compose.subprocess.run",
394
+ side_effect=[
395
+ subprocess.CalledProcessError(
396
+ returncode=1, output="", stderr="TLS handshake timeout", cmd=""
397
+ ),
398
+ subprocess.CalledProcessError(
399
+ returncode=1, output="", stderr="TLS handshake timeout", cmd=""
400
+ ),
401
+ subprocess.CalledProcessError(
402
+ returncode=1, output="", stderr="TLS handshake timeout", cmd=""
403
+ ),
404
+ subprocess.CalledProcessError(
405
+ returncode=1, output="", stderr="TLS handshake timeout", cmd=""
406
+ ),
407
+ subprocess.CalledProcessError(
408
+ returncode=1, output="", stderr="TLS handshake timeout", cmd=""
409
+ ),
410
+ ],
411
+ ) as mock_subprocess_run:
412
+ up(args)
413
+
414
+ # assert multiple failed calls
415
+ assert (
416
+ mock_subprocess_run.mock_calls
417
+ == [
418
+ call(
419
+ [
420
+ "docker",
421
+ "compose",
422
+ "-p",
423
+ "example-service",
424
+ "-f",
425
+ f"{tmp_path}/{DEVSERVICES_DIR_NAME}/{CONFIG_FILE_NAME}",
426
+ "pull",
427
+ "clickhouse",
428
+ "redis",
429
+ ],
430
+ check=True,
431
+ capture_output=True,
432
+ text=True,
433
+ env=mock.ANY,
434
+ )
435
+ ]
436
+ # default is 4 retries (5 tries total)
437
+ * 5
438
+ )
439
+
440
+ mock_create_devservices_network.assert_called_once()
441
+ mock_check_all_containers_healthy.assert_not_called()
442
+ # Capture the printed output
443
+ captured = capsys.readouterr()
444
+
445
+ assert (
446
+ "Failed to start example-service: TLS handshake timeout" in captured.out.strip()
447
+ )
448
+
449
+
450
+ @mock.patch("time.sleep")
451
+ @mock.patch("devservices.utils.state.State.remove_service_entry")
452
+ @mock.patch("devservices.utils.state.State.update_service_entry")
453
+ @mock.patch("devservices.commands.up._create_devservices_network")
454
+ @mock.patch("devservices.commands.up.check_all_containers_healthy")
455
+ @mock.patch(
456
+ "devservices.utils.docker_compose.get_non_remote_services",
457
+ return_value={"clickhouse", "redis"},
458
+ )
459
+ @mock.patch(
460
+ "devservices.commands.up.get_container_names_for_project",
461
+ return_value=["x", "y"],
462
+ )
463
+ def test_up_pull_error_eventual_success(
464
+ mock_get_container_names_for_project: mock.Mock,
465
+ mock_get_non_remote_services: mock.Mock,
466
+ mock_check_all_containers_healthy: mock.Mock,
467
+ mock_create_devservices_network: mock.Mock,
468
+ mock_update_service_entry: mock.Mock,
469
+ mock_remove_service_entry: mock.Mock,
470
+ mock_sleep: mock.Mock,
471
+ capsys: pytest.CaptureFixture[str],
472
+ tmp_path: Path,
473
+ ) -> None:
474
+ config = {
475
+ "x-sentry-service-config": {
476
+ "version": 0.1,
477
+ "service_name": "example-service",
478
+ "dependencies": {
479
+ "redis": {"description": "Redis"},
480
+ "clickhouse": {"description": "Clickhouse"},
481
+ },
482
+ "modes": {"default": ["redis", "clickhouse"]},
483
+ },
484
+ "services": {
485
+ "redis": {"image": "redis:6.2.14-alpine"},
486
+ "clickhouse": {
487
+ "image": "altinity/clickhouse-server:23.8.11.29.altinitystable"
488
+ },
489
+ },
490
+ }
491
+
492
+ create_config_file(tmp_path, config)
493
+ os.chdir(tmp_path)
494
+
495
+ args = Namespace(service_name=None, debug=False, mode="default")
496
+
497
+ with mock.patch(
498
+ "devservices.utils.docker_compose.subprocess.run",
499
+ side_effect=[
500
+ subprocess.CalledProcessError(
501
+ returncode=1, output="", stderr="TLS handshake timeout", cmd=""
502
+ ),
503
+ subprocess.CalledProcessError(
504
+ returncode=1, output="", stderr="TLS handshake timeout", cmd=""
505
+ ),
506
+ subprocess.CompletedProcess(
507
+ args=(),
508
+ returncode=0,
509
+ ),
510
+ subprocess.CompletedProcess(
511
+ args=(),
512
+ returncode=0,
513
+ ),
514
+ ],
515
+ ) as mock_subprocess_run:
516
+ up(args)
517
+
518
+ assert mock_subprocess_run.mock_calls == [
519
+ call(
520
+ [
521
+ "docker",
522
+ "compose",
523
+ "-p",
524
+ "example-service",
525
+ "-f",
526
+ f"{tmp_path}/{DEVSERVICES_DIR_NAME}/{CONFIG_FILE_NAME}",
527
+ "pull",
528
+ "clickhouse",
529
+ "redis",
530
+ ],
531
+ check=True,
532
+ capture_output=True,
533
+ text=True,
534
+ env=mock.ANY,
535
+ )
536
+ ] * 3 + [
537
+ call(
538
+ [
539
+ "docker",
540
+ "compose",
541
+ "-p",
542
+ "example-service",
543
+ "-f",
544
+ f"{tmp_path}/{DEVSERVICES_DIR_NAME}/{CONFIG_FILE_NAME}",
545
+ "up",
546
+ "clickhouse",
547
+ "redis",
548
+ "-d",
549
+ ],
550
+ check=True,
551
+ capture_output=True,
552
+ text=True,
553
+ env=mock.ANY,
554
+ )
555
+ ]
556
+
557
+ mock_create_devservices_network.assert_called_once()
558
+ captured = capsys.readouterr()
559
+
560
+ assert "example-service started" in captured.out.strip()
561
+
562
+
348
563
  @mock.patch("devservices.utils.state.State.remove_service_entry")
349
564
  @mock.patch("devservices.utils.state.State.update_service_entry")
350
565
  @mock.patch("devservices.commands.up._create_devservices_network")
File without changes
File without changes