quapp-common 0.0.13.dev7__tar.gz → 0.0.13.dev8__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 (92) hide show
  1. {quapp_common-0.0.13.dev7/quapp_common.egg-info → quapp_common-0.0.13.dev8}/PKG-INFO +1 -1
  2. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/pyproject.toml +1 -1
  3. quapp_common-0.0.13.dev8/quapp_common/component/backend/job_heartbeat.py +83 -0
  4. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/backend/job_manager.py +6 -0
  5. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8/quapp_common.egg-info}/PKG-INFO +1 -1
  6. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common.egg-info/SOURCES.txt +2 -0
  7. quapp_common-0.0.13.dev8/tests/test_job_heartbeat.py +115 -0
  8. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/LICENSE +0 -0
  9. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/README.md +0 -0
  10. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/__init__.py +0 -0
  11. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/async_tasks/__init__.py +0 -0
  12. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/async_tasks/async_invocation_task.py +0 -0
  13. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/async_tasks/async_task.py +0 -0
  14. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/async_tasks/export_circuit_task.py +0 -0
  15. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/async_tasks/post_processing_task.py +0 -0
  16. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/__init__.py +0 -0
  17. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/backend/__init__.py +0 -0
  18. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/backend/invocation.py +0 -0
  19. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/backend/job_fetcher.py +0 -0
  20. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/backend/job_fetching.py +0 -0
  21. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/bridge.py +0 -0
  22. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/callback/__init__.py +0 -0
  23. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/callback/update_job_metadata.py +0 -0
  24. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/circuit_adapter.py +0 -0
  25. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/device/__init__.py +0 -0
  26. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/device/device_selection.py +0 -0
  27. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/dispatcher.py +0 -0
  28. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/component/result_serializer.py +0 -0
  29. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/config/__init__.py +0 -0
  30. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/config/logging_config.py +0 -0
  31. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/config/thread_config.py +0 -0
  32. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/__init__.py +0 -0
  33. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/async_task/__init__.py +0 -0
  34. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
  35. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
  36. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
  37. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/backend/__init__.py +0 -0
  38. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/backend/backend_information.py +0 -0
  39. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/callback/__init__.py +0 -0
  40. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/callback/callback_url.py +0 -0
  41. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/device/__init__.py +0 -0
  42. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/device/circuit_running_option.py +0 -0
  43. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/promise/__init__.py +0 -0
  44. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/promise/post_processing_promise.py +0 -0
  45. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/promise/promise.py +0 -0
  46. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/request/__init__.py +0 -0
  47. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/request/invocation_request.py +0 -0
  48. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/request/job_fetching_request.py +0 -0
  49. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/request/request.py +0 -0
  50. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/response/__init__.py +0 -0
  51. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/response/authentication.py +0 -0
  52. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/response/custom_header.py +0 -0
  53. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/data/response/job_response.py +0 -0
  54. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/__init__.py +0 -0
  55. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/base_enum.py +0 -0
  56. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/http_header.py +0 -0
  57. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/invocation_step.py +0 -0
  58. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/language.py +0 -0
  59. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/media_type.py +0 -0
  60. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/processing_unit.py +0 -0
  61. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/provider_tag.py +0 -0
  62. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/sdk.py +0 -0
  63. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/status/__init__.py +0 -0
  64. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/status/job_status.py +0 -0
  65. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/status/status_code.py +0 -0
  66. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/enum/token_type.py +0 -0
  67. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/factory/__init__.py +0 -0
  68. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/factory/device_factory.py +0 -0
  69. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/factory/handler_factory.py +0 -0
  70. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/factory/provider_factory.py +0 -0
  71. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/handler/__init__.py +0 -0
  72. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/handler/handler.py +0 -0
  73. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/model/__init__.py +0 -0
  74. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/model/device/__init__.py +0 -0
  75. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/model/device/custom_device.py +0 -0
  76. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/model/device/device.py +0 -0
  77. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/model/invocation.py +0 -0
  78. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/model/provider/__init__.py +0 -0
  79. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/model/provider/provider.py +0 -0
  80. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/util/__init__.py +0 -0
  81. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/util/file_utils.py +0 -0
  82. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/util/http_utils.py +0 -0
  83. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/util/json_parser_utils.py +0 -0
  84. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common/util/response_utils.py +0 -0
  85. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common.egg-info/dependency_links.txt +0 -0
  86. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common.egg-info/requires.txt +0 -0
  87. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/quapp_common.egg-info/top_level.txt +0 -0
  88. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/setup.cfg +0 -0
  89. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/tests/test_circuit_adapter.py +0 -0
  90. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/tests/test_json_parser_utils.py +0 -0
  91. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/tests/test_language_dispatcher.py +0 -0
  92. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev8}/tests/test_result_serializer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.13.dev7
3
+ Version: 0.0.13.dev8
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quapp-common"
7
- version = "0.0.13.dev7"
7
+ version = "0.0.13.dev8"
8
8
  description = "Quapp common library supporting Quapp Platform for Quantum Computing"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "CITYNOW Co. Ltd. ", email = "corp@citynow.vn" }]
@@ -0,0 +1,83 @@
1
+ # Quapp Platform Project
2
+ # job_heartbeat.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ import os
6
+ import threading
7
+
8
+ import requests
9
+
10
+ from ...config.logging_config import logger
11
+
12
+ DEFAULT_INTERVAL_SEC = 30.0
13
+ REQUEST_TIMEOUT_SEC = 5.0
14
+ TERMINAL_STATUSES = ('DONE', 'FAILED')
15
+
16
+ _lock = threading.Lock()
17
+ _thread = None
18
+ _stop_event = threading.Event()
19
+
20
+
21
+ def heartbeat_interval_sec() -> float:
22
+ raw = os.getenv('HEARTBEAT_INTERVAL_SEC', '')
23
+ try:
24
+ value = float(raw)
25
+ if value > 0:
26
+ return value
27
+ except ValueError:
28
+ pass
29
+ return DEFAULT_INTERVAL_SEC
30
+
31
+
32
+ def beat_once() -> int:
33
+ from .job_manager import JobManager, META, STATUS, SCHEDULER_CALLBACK_URL
34
+
35
+ sent = 0
36
+ for job_id, data in JobManager.list_jobs().items():
37
+ if data.get(STATUS) in TERMINAL_STATUSES:
38
+ continue
39
+ url = (data.get(META) or {}).get(SCHEDULER_CALLBACK_URL)
40
+ if not url:
41
+ continue
42
+ try:
43
+ response = requests.patch(
44
+ url, json={'status': 'RUNNING', 'heartbeat': True},
45
+ timeout=REQUEST_TIMEOUT_SEC)
46
+ logger.debug(
47
+ f'Heartbeat for job {job_id} returned {response.status_code}')
48
+ sent += 1
49
+ except Exception as exception:
50
+ logger.debug(f'Heartbeat for job {job_id} failed: {exception}')
51
+ return sent
52
+
53
+
54
+ def _run() -> None:
55
+ while True:
56
+ try:
57
+ beat_once()
58
+ except Exception as exception:
59
+ logger.exception(f'Heartbeat loop error: {exception}')
60
+ if _stop_event.wait(heartbeat_interval_sec()):
61
+ return
62
+
63
+
64
+ def ensure_started() -> None:
65
+ global _thread
66
+ with _lock:
67
+ if _thread is not None and _thread.is_alive():
68
+ return
69
+ _stop_event.clear()
70
+ _thread = threading.Thread(target=_run, name='job-heartbeat',
71
+ daemon=True)
72
+ _thread.start()
73
+ logger.info(
74
+ f'Job heartbeat started with interval {heartbeat_interval_sec()}s')
75
+
76
+
77
+ def stop() -> None:
78
+ global _thread
79
+ with _lock:
80
+ _stop_event.set()
81
+ if _thread is not None:
82
+ _thread.join(timeout=1.0)
83
+ _thread = None
@@ -66,6 +66,9 @@ class JobManager:
66
66
  """
67
67
  Stop any listener threads in this process and shutdown Manager.
68
68
  """
69
+ from .job_heartbeat import stop
70
+ stop()
71
+
69
72
  # stop listeners
70
73
  for key, (t, stop_ev) in list(cls._listener_threads.items()):
71
74
  stop_ev.set()
@@ -100,6 +103,9 @@ class JobManager:
100
103
  cls._jobs[job_id] = data # proxy is process-safe
101
104
  cls._publish(job_id, {"event": "added", **data})
102
105
 
106
+ from .job_heartbeat import ensure_started
107
+ ensure_started()
108
+
103
109
  @classmethod
104
110
  def update_status(cls, job_id: str, status: str,
105
111
  meta_update: Optional[Dict[str, Any]] = None) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.13.dev7
3
+ Version: 0.0.13.dev8
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -21,6 +21,7 @@ quapp_common/component/backend/__init__.py
21
21
  quapp_common/component/backend/invocation.py
22
22
  quapp_common/component/backend/job_fetcher.py
23
23
  quapp_common/component/backend/job_fetching.py
24
+ quapp_common/component/backend/job_heartbeat.py
24
25
  quapp_common/component/backend/job_manager.py
25
26
  quapp_common/component/callback/__init__.py
26
27
  quapp_common/component/callback/update_job_metadata.py
@@ -83,6 +84,7 @@ quapp_common/util/http_utils.py
83
84
  quapp_common/util/json_parser_utils.py
84
85
  quapp_common/util/response_utils.py
85
86
  tests/test_circuit_adapter.py
87
+ tests/test_job_heartbeat.py
86
88
  tests/test_json_parser_utils.py
87
89
  tests/test_language_dispatcher.py
88
90
  tests/test_result_serializer.py
@@ -0,0 +1,115 @@
1
+ import unittest
2
+ from unittest.mock import patch, MagicMock
3
+
4
+ from quapp_common.component.backend import job_heartbeat
5
+ from quapp_common.component.backend.job_manager import JobManager
6
+
7
+
8
+ class TestHeartbeatInterval(unittest.TestCase):
9
+ def test_default_interval(self):
10
+ with patch.dict('os.environ', {}, clear=False):
11
+ import os
12
+ os.environ.pop('HEARTBEAT_INTERVAL_SEC', None)
13
+ self.assertEqual(job_heartbeat.heartbeat_interval_sec(),
14
+ job_heartbeat.DEFAULT_INTERVAL_SEC)
15
+
16
+ def test_env_override(self):
17
+ with patch.dict('os.environ', {'HEARTBEAT_INTERVAL_SEC': '10'}):
18
+ self.assertEqual(job_heartbeat.heartbeat_interval_sec(), 10.0)
19
+
20
+ def test_invalid_env_falls_back(self):
21
+ with patch.dict('os.environ', {'HEARTBEAT_INTERVAL_SEC': 'abc'}):
22
+ self.assertEqual(job_heartbeat.heartbeat_interval_sec(),
23
+ job_heartbeat.DEFAULT_INTERVAL_SEC)
24
+
25
+ def test_non_positive_env_falls_back(self):
26
+ with patch.dict('os.environ', {'HEARTBEAT_INTERVAL_SEC': '0'}):
27
+ self.assertEqual(job_heartbeat.heartbeat_interval_sec(),
28
+ job_heartbeat.DEFAULT_INTERVAL_SEC)
29
+
30
+
31
+ class TestBeatOnce(unittest.TestCase):
32
+ def setUp(self):
33
+ for job_id in list(JobManager.list_jobs()):
34
+ JobManager.remove_job(job_id)
35
+
36
+ def tearDown(self):
37
+ for job_id in list(JobManager.list_jobs()):
38
+ JobManager.remove_job(job_id)
39
+ job_heartbeat.stop()
40
+
41
+ def test_beats_active_job_with_url(self):
42
+ JobManager.add_job('job-active', status='RUNNING', meta={
43
+ 'scheduler_callback_url': 'http://scheduler/v1/callbacks/job-active?gen=2'})
44
+ with patch.object(job_heartbeat.requests, 'patch',
45
+ return_value=MagicMock(status_code=200)) as mock_patch:
46
+ sent = job_heartbeat.beat_once()
47
+ self.assertEqual(sent, 1)
48
+ mock_patch.assert_called_once_with(
49
+ 'http://scheduler/v1/callbacks/job-active?gen=2',
50
+ json={'status': 'RUNNING', 'heartbeat': True},
51
+ timeout=job_heartbeat.REQUEST_TIMEOUT_SEC)
52
+
53
+ def test_skips_terminal_jobs(self):
54
+ JobManager.add_job('job-done', status='DONE', meta={
55
+ 'scheduler_callback_url': 'http://scheduler/v1/callbacks/job-done'})
56
+ JobManager.add_job('job-failed', status='FAILED', meta={
57
+ 'scheduler_callback_url': 'http://scheduler/v1/callbacks/job-failed'})
58
+ with patch.object(job_heartbeat.requests, 'patch') as mock_patch:
59
+ sent = job_heartbeat.beat_once()
60
+ self.assertEqual(sent, 0)
61
+ mock_patch.assert_not_called()
62
+
63
+ def test_skips_job_without_url(self):
64
+ JobManager.add_job('job-no-url', status='RUNNING', meta={})
65
+ with patch.object(job_heartbeat.requests, 'patch') as mock_patch:
66
+ sent = job_heartbeat.beat_once()
67
+ self.assertEqual(sent, 0)
68
+ mock_patch.assert_not_called()
69
+
70
+ def test_network_error_does_not_raise(self):
71
+ JobManager.add_job('job-net', status='RUNNING', meta={
72
+ 'scheduler_callback_url': 'http://scheduler/v1/callbacks/job-net'})
73
+ with patch.object(job_heartbeat.requests, 'patch',
74
+ side_effect=ConnectionError('down')):
75
+ sent = job_heartbeat.beat_once()
76
+ self.assertEqual(sent, 0)
77
+
78
+ def test_pending_job_is_beaten(self):
79
+ JobManager.add_job('job-pending', status='PENDING', meta={
80
+ 'scheduler_callback_url': 'http://scheduler/v1/callbacks/job-pending'})
81
+ with patch.object(job_heartbeat.requests, 'patch',
82
+ return_value=MagicMock(status_code=200)) as mock_patch:
83
+ sent = job_heartbeat.beat_once()
84
+ self.assertEqual(sent, 1)
85
+
86
+
87
+ class TestDaemonLifecycle(unittest.TestCase):
88
+ def tearDown(self):
89
+ job_heartbeat.stop()
90
+ for job_id in list(JobManager.list_jobs()):
91
+ JobManager.remove_job(job_id)
92
+
93
+ def test_ensure_started_is_idempotent(self):
94
+ job_heartbeat.ensure_started()
95
+ first = job_heartbeat._thread
96
+ job_heartbeat.ensure_started()
97
+ self.assertIs(job_heartbeat._thread, first)
98
+ self.assertTrue(first.is_alive())
99
+
100
+ def test_stop_terminates_thread(self):
101
+ job_heartbeat.ensure_started()
102
+ thread = job_heartbeat._thread
103
+ job_heartbeat.stop()
104
+ self.assertFalse(thread.is_alive())
105
+ self.assertIsNone(job_heartbeat._thread)
106
+
107
+ def test_add_job_starts_daemon(self):
108
+ job_heartbeat.stop()
109
+ JobManager.add_job('job-autostart', status='RUNNING', meta={})
110
+ self.assertIsNotNone(job_heartbeat._thread)
111
+ self.assertTrue(job_heartbeat._thread.is_alive())
112
+
113
+
114
+ if __name__ == '__main__':
115
+ unittest.main()