primitive 0.2.39__py3-none-any.whl → 0.2.42__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.
primitive/__about__.py CHANGED
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2024-present Dylan Stein <dylan@primitive.tech>
1
+ # SPDX-FileCopyrightText: 2025-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.2.39"
4
+ __version__ = "0.2.42"
@@ -7,13 +7,11 @@ from loguru import logger
7
7
  from primitive.__about__ import __version__
8
8
  from primitive.agent.runner import Runner
9
9
  from primitive.agent.uploader import Uploader
10
- from primitive.db.models import JobRun
11
- from primitive.db.sqlite import wait_for_db
12
10
  from primitive.utils.actions import BaseAction
13
11
 
14
12
 
15
13
  class Agent(BaseAction):
16
- def execute(self, job_run_id: Optional[str] = None):
14
+ def start(self, job_run_id: Optional[str] = None):
17
15
  logger.remove()
18
16
  logger.add(
19
17
  sink=sys.stderr,
@@ -31,9 +29,6 @@ class Agent(BaseAction):
31
29
  logger.info("Running in container...")
32
30
  RUNNING_IN_CONTAINER = True
33
31
 
34
- # Wait for monitor to make database
35
- wait_for_db()
36
-
37
32
  # Create uploader
38
33
  uploader = Uploader(primitive=self.primitive)
39
34
 
@@ -42,37 +37,79 @@ class Agent(BaseAction):
42
37
  logger.debug("Scanning for files to upload...")
43
38
  uploader.scan()
44
39
 
45
- db_job_run = JobRun.objects.first()
46
-
47
- if not db_job_run:
48
- sleep_amount = 5
49
- logger.debug(
50
- f"No pending job runs... [sleeping {sleep_amount} seconds]"
51
- )
52
- sleep(sleep_amount)
40
+ logger.debug("Checking for pending job runs for this device...")
41
+
42
+ # From Dylan June 30th:
43
+ # If passed an explicit job_run_id:
44
+ # - check if the JobRun exists in the API
45
+ # - if it does, set it to request_in_progress
46
+ # - if it does not, log an error and stop execution
47
+ # If no job_run_id is passed:
48
+ # - verify that this is a Node with an active Reservation
49
+ # - if the Reservation is active AND it has a JobRun associated with it,
50
+ # then query for that JobRun
51
+ # - if no JobRuns are found in the API, wait for another active reservation
52
+ # - if a JobRun is found, set it to request_in_progress
53
+ # - then wait for the JobRun to be in_progress from the API
54
+
55
+ active_reservation_id = None
56
+ job_run_data: dict = {}
57
+
58
+ if RUNNING_IN_CONTAINER and job_run_id:
59
+ job_run_result = self.primitive.jobs.get_job_run(id=job_run_id)
60
+ if job_run_result.data:
61
+ job_run_data = job_run_result.data.get("jobRun", {})
62
+ else:
63
+ hardware = self.primitive.hardware.get_own_hardware_details()
64
+ # fetch the latest hardware and activeReservation details
65
+ if active_reservation_data := hardware["activeReservation"]:
66
+ active_reservation_id = active_reservation_data.get("id", None)
67
+
68
+ if active_reservation_id is not None:
69
+ job_run_data = (
70
+ self.primitive.reservations.get_job_run_for_reservation_id(
71
+ reservation_id=active_reservation_id
72
+ )
73
+ )
74
+ job_run_id = job_run_data.get("id", None)
75
+
76
+ if (
77
+ len(job_run_data.keys()) == 0
78
+ or not job_run_data.get("id")
79
+ or job_run_id is None
80
+ ):
81
+ if RUNNING_IN_CONTAINER:
82
+ logger.info("Running in container, exiting due to no JobRun.")
83
+ break
84
+ logger.debug("No pending Job Run found, sleeping...")
85
+ sleep(5)
53
86
  continue
54
87
 
55
- api_job_run_data = self.primitive.jobs.get_job_run(
56
- id=db_job_run.job_run_id,
88
+ logger.debug("Found pending Job Run")
89
+ logger.debug(f"Job Run ID: {job_run_data.get('id')}")
90
+ logger.debug(f"Job Name: {job_run_data.get('job').get('name')}")
91
+
92
+ logger.info(
93
+ f"Setting JobRun {job_run_data.get('job').get('name')} to request_in_progress"
94
+ )
95
+ # we are setting to request_in_progress here which puts a started_at time on the JobRun in the API's database
96
+ # any time spent pulling Git repositories, setting up, etc, counts as compute time
97
+ job_run_result = self.primitive.jobs.job_run_update(
98
+ id=job_run_id, status="request_in_progress"
57
99
  )
58
100
 
59
- if not api_job_run_data or not api_job_run_data.data:
60
- logger.error(
61
- f"Job Run {db_job_run.job_run_id} not found in API, deleting from DB"
101
+ while job_run_data["status"] != "in_progress":
102
+ logger.info(
103
+ f"Waiting for JobRun {job_run_data.get('name')} to be in_progress"
62
104
  )
63
- JobRun.objects.filter_by(job_run_id=db_job_run.job_run_id).delete()
64
- continue
65
-
66
- api_job_run = api_job_run_data.data["jobRun"]
67
-
68
- logger.debug("Found pending Job Run")
69
- logger.debug(f"Job Run ID: {api_job_run.get('id')}")
70
- logger.debug(f"Job Name: {api_job_run.get('name')}")
105
+ sleep(1)
106
+ job_run_result = self.primitive.jobs.get_job_run(id=job_run_id)
107
+ if job_run_result.data is not None:
108
+ job_run_data = job_run_result.data.get("jobRun", {})
71
109
 
72
110
  runner = Runner(
73
111
  primitive=self.primitive,
74
- job_run=api_job_run,
75
- # max_log_size=500 * 1024,
112
+ job_run=job_run_data,
76
113
  )
77
114
 
78
115
  try:
@@ -82,19 +119,18 @@ class Agent(BaseAction):
82
119
  f"Exception while initializing runner: {exception}"
83
120
  )
84
121
  self.primitive.jobs.job_run_update(
85
- id=api_job_run.get("id"),
122
+ id=job_run_id,
86
123
  status="request_completed",
87
124
  conclusion="failure",
88
125
  )
89
- JobRun.objects.filter_by(job_run_id=api_job_run.get("id")).delete()
90
126
  continue
91
127
 
92
128
  try:
93
- runner.execute()
129
+ runner.execute_job_run()
94
130
  except Exception as exception:
95
131
  logger.exception(f"Exception while executing job: {exception}")
96
132
  self.primitive.jobs.job_run_update(
97
- id=api_job_run.get("id"),
133
+ id=job_run_id,
98
134
  status="request_completed",
99
135
  conclusion="failure",
100
136
  )
@@ -12,4 +12,4 @@ if typing.TYPE_CHECKING:
12
12
  def cli(context, job_run_id: typing.Optional[str] = None):
13
13
  """agent"""
14
14
  primitive: Primitive = context.obj.get("PRIMITIVE")
15
- primitive.agent.execute(job_run_id=job_run_id)
15
+ primitive.agent.start(job_run_id=job_run_id)
primitive/agent/runner.py CHANGED
@@ -10,10 +10,10 @@ from typing import Dict, List, TypedDict
10
10
  import yaml
11
11
  from loguru import logger
12
12
 
13
- from ..db.models import JobRun
14
- from ..utils.cache import get_artifacts_cache, get_logs_cache, get_sources_cache
15
- from ..utils.logging import fmt, log_context
16
- from ..utils.shell import env_to_dict
13
+ from primitive.utils.cache import get_artifacts_cache, get_logs_cache, get_sources_cache
14
+ from primitive.utils.logging import fmt, log_context
15
+ from primitive.utils.psutil import kill_process_and_children
16
+ from primitive.utils.shell import env_to_dict
17
17
 
18
18
  try:
19
19
  from yaml import CLoader as Loader
@@ -109,27 +109,21 @@ class Runner:
109
109
  self.job_settings["rootDirectory"]
110
110
  )
111
111
 
112
- db_config = self.job_settings.get("config", None)
113
- if db_config:
114
- logger.info(f"Using job config from database for {self.job['slug']}")
115
- self.config = db_config
116
- else:
117
- # Attempt to parse the job yaml file
118
- job_filename = self.job_settings["repositoryFilename"]
119
- logger.info(f"Scanning directory for job file {job_filename}")
112
+ job_filename = self.job_settings["repositoryFilename"]
113
+ logger.info(f"Scanning directory for job file {job_filename}")
120
114
 
121
- job_config_file = Path(self.source_dir / ".primitive" / job_filename)
115
+ job_config_file = Path(self.source_dir / ".primitive" / job_filename)
122
116
 
123
- if job_config_file.exists():
124
- logger.info(
125
- f"Found job description for {self.job['slug']} at {job_config_file}"
126
- )
127
- self.config = yaml.load(open(job_config_file, "r"), Loader=Loader)
128
- else:
129
- logger.error(
130
- f"No job description with matching filename '{job_filename}' found"
131
- )
132
- raise FileNotFoundError
117
+ if job_config_file.exists():
118
+ logger.info(
119
+ f"Found job description for {self.job['slug']} at {job_config_file}"
120
+ )
121
+ self.config = yaml.load(open(job_config_file, "r"), Loader=Loader)
122
+ else:
123
+ logger.error(
124
+ f"No job description with matching filename '{job_filename}' found"
125
+ )
126
+ raise FileNotFoundError
133
127
 
134
128
  # Setup initial process environment
135
129
  self.initial_env = os.environ
@@ -147,15 +141,11 @@ class Runner:
147
141
  )
148
142
 
149
143
  @log_context(label="execute")
150
- def execute(self) -> None:
151
- logger.info(f"Executing {self.job['slug']} job")
152
- self.primitive.jobs.job_run_update(
153
- self.job_run["id"], status="request_in_progress"
154
- )
155
-
144
+ def execute_job_run(self) -> None:
156
145
  self.modified_env = {**self.initial_env}
157
146
  task_failed = False
158
147
  cancelled = False
148
+ timed_out = False
159
149
 
160
150
  for task in self.config["executes"]:
161
151
  # Everything inside this loop should be contextualized with the task label
@@ -171,6 +161,9 @@ class Runner:
171
161
  if status_value == "completed" and conclusion_value == "cancelled":
172
162
  cancelled = True
173
163
  break
164
+ if status_value == "completed" and conclusion_value == "timed_out":
165
+ timed_out = True
166
+ break
174
167
 
175
168
  # Everything within this block should be contextualized as user logs
176
169
  with logger.contextualize(type="user"):
@@ -186,11 +179,17 @@ class Runner:
186
179
  conclusion_value = status.data["jobRun"]["conclusion"]
187
180
  if status_value == "completed" and conclusion_value == "cancelled":
188
181
  cancelled = True
182
+ if status_value == "completed" and conclusion_value == "timed_out":
183
+ timed_out = True
189
184
 
190
185
  if cancelled:
191
186
  logger.warning("Job cancelled by user")
192
187
  return
193
188
 
189
+ if timed_out:
190
+ logger.error("Job timed out")
191
+ return
192
+
194
193
  conclusion = "success"
195
194
  if task_failed:
196
195
  conclusion = "failure"
@@ -270,9 +269,17 @@ class Runner:
270
269
  stderr=asyncio.subprocess.PIPE,
271
270
  )
272
271
 
273
- JobRun.objects.filter_by(job_run_id=self.job_run["id"]).update(
274
- {"pid": process.pid}
275
- )
272
+ try:
273
+ await self.primitive.jobs.ajob_run_update(
274
+ self.job_run["id"],
275
+ parent_pid=process.pid,
276
+ )
277
+ except ValueError:
278
+ logger.error(
279
+ f"Failed to update job run {self.job_run['id']} with process PID {process.pid}"
280
+ )
281
+ kill_process_and_children(pid=process.pid)
282
+ return False
276
283
 
277
284
  stdout_failed, stderr_failed = await asyncio.gather(
278
285
  self.log_cmd(
@@ -289,10 +296,6 @@ class Runner:
289
296
  f"Finished executing command {i + 1}/{len(commands)}: {cmd} with return code {returncode}"
290
297
  )
291
298
 
292
- JobRun.objects.filter_by(job_run_id=self.job_run["id"]).update(
293
- {"pid": None}
294
- )
295
-
296
299
  if returncode > 0:
297
300
  logger.error(
298
301
  f"Task {task['label']} failed on '{cmd}' with return code {returncode}"
primitive/client.py CHANGED
@@ -1,8 +1,9 @@
1
+ from typing import Optional
2
+
1
3
  from gql import Client
2
4
  from loguru import logger
3
5
  from rich.logging import RichHandler
4
6
  from rich.traceback import install
5
- from typing import Optional
6
7
 
7
8
  from .agent.actions import Agent
8
9
  from .auth.actions import Auth
@@ -12,11 +13,11 @@ from .files.actions import Files
12
13
  from .git.actions import Git
13
14
  from .hardware.actions import Hardware
14
15
  from .jobs.actions import Jobs
16
+ from .monitor.actions import Monitor
15
17
  from .organizations.actions import Organizations
16
18
  from .projects.actions import Projects
17
19
  from .provisioning.actions import Provisioning
18
20
  from .reservations.actions import Reservations
19
- from .monitor.actions import Monitor
20
21
  from .utils.config import read_config_file
21
22
 
22
23
 
primitive/jobs/actions.py CHANGED
@@ -115,6 +115,7 @@ class Jobs(BaseAction):
115
115
  conclusion: str = None,
116
116
  file_ids: Optional[List[str]] = [],
117
117
  number_of_files_produced: Optional[int] = None,
118
+ parent_pid: Optional[int] = None,
118
119
  ):
119
120
  mutation = gql(job_run_update_mutation)
120
121
  input = {"id": id}
@@ -126,12 +127,42 @@ class Jobs(BaseAction):
126
127
  input["files"] = file_ids
127
128
  if number_of_files_produced is not None:
128
129
  input["numberOfFilesProduced"] = number_of_files_produced
130
+ if parent_pid is not None:
131
+ input["parentPid"] = parent_pid
129
132
  variables = {"input": input}
130
133
  result = self.primitive.session.execute(
131
134
  mutation, variable_values=variables, get_execution_result=True
132
135
  )
133
136
  return result
134
137
 
138
+ @guard
139
+ async def ajob_run_update(
140
+ self,
141
+ id: str,
142
+ status: str = None,
143
+ conclusion: str = None,
144
+ file_ids: Optional[List[str]] = [],
145
+ number_of_files_produced: Optional[int] = None,
146
+ parent_pid: Optional[int] = None,
147
+ ):
148
+ mutation = gql(job_run_update_mutation)
149
+ input = {"id": id}
150
+ if status:
151
+ input["status"] = status
152
+ if conclusion:
153
+ input["conclusion"] = conclusion
154
+ if file_ids and len(file_ids) > 0:
155
+ input["files"] = file_ids
156
+ if number_of_files_produced is not None:
157
+ input["numberOfFilesProduced"] = number_of_files_produced
158
+ if parent_pid is not None:
159
+ input["parentPid"] = parent_pid
160
+ variables = {"input": input}
161
+ result = await self.primitive.session.execute_async(
162
+ mutation, variable_values=variables, get_execution_result=True
163
+ )
164
+ return result
165
+
135
166
  @guard
136
167
  def github_access_token_for_job_run(self, job_run_id: str):
137
168
  query = gql(github_app_token_for_job_run_query)
@@ -50,5 +50,6 @@ fragment JobRunStatusFragment on JobRun {
50
50
  id
51
51
  status
52
52
  conclusion
53
+ parentPid
53
54
  }
54
55
  """
@@ -2,14 +2,14 @@ import sys
2
2
  from time import sleep
3
3
  from typing import Optional
4
4
 
5
- import psutil
6
5
  from loguru import logger
7
6
 
8
7
  from primitive.__about__ import __version__
9
- from primitive.db.models import JobRun
10
- from primitive.db.sqlite import init, wait_for_db
11
8
  from primitive.utils.actions import BaseAction
12
- from primitive.utils.exceptions import P_CLI_100
9
+ from primitive.utils.exceptions import P_CLI_100, P_CLI_101
10
+ from primitive.utils.psutil import kill_process_and_children
11
+
12
+ MAX_GET_STATUS_TIMEOUT = 30
13
13
 
14
14
 
15
15
  class Monitor(BaseAction):
@@ -44,194 +44,143 @@ class Monitor(BaseAction):
44
44
  logger.exception(f"Error checking in hardware: {exception}")
45
45
  sys.exit(1)
46
46
 
47
- # Initialize the database
48
- init()
49
- wait_for_db()
47
+ # From Dylan on June 30th:
48
+ # If passed an explicit job_run_id we know it is running in a container.
49
+ # If no job_run_id is passed, we need to check that this device has an active reservation.
50
+ # Fetch the active reservations. If it exists AND has a JobRun associated with it:
51
+ # - check if the JobRun exists in the API
52
+ # - if it does exist, check if it is already running
53
+ # - if it is in status [pending, request_in_progress, in_progress] where we should wait for the PID
54
+ # - if it is in status [request_completed, completed] and there is a PID, kill it
55
+ # Finally, if running in a container, kill the process.
56
+ # Else, wait for a new active reservation to be created.
50
57
 
51
58
  try:
52
- if job_run_id is not None:
53
- if not JobRun.objects.filter_by(job_run_id=job_run_id).exists():
54
- logger.debug(f"Job run {job_run_id} does not exist in database.")
55
- logger.debug(f"Creating job run in database: {job_run_id}")
56
- JobRun.objects.create(job_run_id=job_run_id, pid=None)
57
-
59
+ active_reservation_data = None
60
+ previous_reservation_id = None
58
61
  active_reservation_id = None
59
- active_reservation_pk = None
60
62
 
61
63
  while True:
62
- # FIRST, check for jobs in the database that are running
63
- db_job_runs = JobRun.objects.all()
64
- for job_run in db_job_runs:
65
- # first check if the job run completed already
66
- status = self.primitive.jobs.get_job_status(job_run.job_run_id)
67
- if status is None or status.data is None:
68
- logger.error(
69
- f"Error fetching status of <JobRun {job_run.job_run_id}>."
64
+ # this block determines if there is a reservation at all
65
+ # handles cleanup of old reservations
66
+ # obtains an active JobRun's ID
67
+ if not RUNNING_IN_CONTAINER:
68
+ hardware = self.primitive.hardware.get_own_hardware_details()
69
+ # fetch the latest hardware and activeReservation details
70
+ if active_reservation_data := hardware["activeReservation"]:
71
+ active_reservation_id = active_reservation_data.get("id", None)
72
+ if previous_reservation_id is None:
73
+ previous_reservation_id = active_reservation_id
74
+ else:
75
+ active_reservation_data = None
76
+ active_reservation_id = None
77
+
78
+ # if there is no activeReservation or previous reservation, sync + sleep
79
+ if (
80
+ active_reservation_data is None
81
+ and active_reservation_id is None
82
+ and previous_reservation_id is None
83
+ ):
84
+ self.primitive.hardware.check_in_http(
85
+ is_available=True, is_online=True
70
86
  )
71
- continue
87
+ self.primitive.hardware._sync_children(hardware=hardware)
72
88
 
73
- status_value = status.data["jobRun"]["status"]
74
- if status_value == "completed":
75
- logger.debug(
76
- f"Job run {job_run.job_run_id} is completed. Removing from database."
89
+ sleep_amount = 5
90
+ logger.info(
91
+ f"No active reservation found... [sleeping {sleep_amount} seconds]"
77
92
  )
78
- JobRun.objects.filter_by(job_run_id=job_run.job_run_id).delete()
93
+ sleep(sleep_amount)
79
94
  continue
80
95
 
81
- if job_run.pid is None:
82
- pid_sleep_amount = 0.1
83
- logger.debug(
84
- f"Job run {job_run.job_run_id} has no PID. Agent has not started."
96
+ # if there is a previous_reservation_id but no activeReservation, cleanup
97
+ elif active_reservation_data is None and previous_reservation_id:
98
+ logger.info(
99
+ f"Cleaning up previous reservation {previous_reservation_id}..."
85
100
  )
86
- logger.debug(
87
- f"Sleeping {pid_sleep_amount} seconds before checking again..."
101
+ self.primitive.provisioning.remove_reservation_authorized_keys(
102
+ reservation_id=previous_reservation_id
88
103
  )
89
- sleep(pid_sleep_amount)
90
- pid_sleep_amount += 0.1
91
- continue
92
-
93
- logger.debug(
94
- f"Checking process PID {job_run.pid} for JobRun {job_run.job_run_id}..."
95
- )
96
-
97
- status = self.primitive.jobs.get_job_status(job_run.job_run_id)
98
- if status is None or status.data is None:
99
- logger.error(
100
- f"Error fetching status of <JobRun {job_run.job_run_id}>."
104
+ job_run_data = (
105
+ self.primitive.reservations.get_job_run_for_reservation_id(
106
+ reservation_id=previous_reservation_id
107
+ )
101
108
  )
102
- continue
103
-
104
- status_value = status.data["jobRun"]["status"]
105
- conclusion_value = status.data["jobRun"]["conclusion"]
106
-
107
- logger.debug(f"- Status: {status_value}")
108
- logger.debug(f"- Conclusion: {conclusion_value}")
109
-
110
- try:
111
- parent = psutil.Process(job_run.pid)
112
- except psutil.NoSuchProcess:
113
- logger.debug("Process not found")
114
- continue
115
-
116
- children = parent.children(recursive=True)
117
-
118
- if status_value == "completed" and conclusion_value == "cancelled":
119
- logger.warning("Job cancelled by user")
120
- for child in children:
121
- logger.debug(f"Killing child process {child.pid}...")
122
- child.kill()
123
-
124
- logger.debug(f"Killing parent process {parent.pid}...")
125
- parent.kill()
126
-
127
- if status != "completed":
128
- sleep(1)
129
- continue
130
-
131
- if RUNNING_IN_CONTAINER:
132
- if len(db_job_runs) == 0:
133
- # if we get here and we're running in a container,
134
- # it means the job run is complete and there is nothing left in the database
135
- # so we can exit
136
- logger.debug("Running in container, initial job complete.")
137
- sys.exit(0)
138
- else:
139
- continue
140
-
141
- # Second, check for active reservations
142
- hardware = self.primitive.hardware.get_own_hardware_details()
143
- if hardware["activeReservation"]:
144
- if (
145
- hardware["activeReservation"]["id"] != active_reservation_id
146
- or hardware["activeReservation"]["pk"] != active_reservation_pk
109
+ job_run_id = job_run_data.get("id")
110
+ previous_reservation_id = None
111
+
112
+ # if we are on the new reservation
113
+ elif (
114
+ (previous_reservation_id is not None)
115
+ and (active_reservation_id is not None)
116
+ and (previous_reservation_id == active_reservation_id)
147
117
  ):
148
- logger.info("New reservation for this hardware.")
149
- active_reservation_id = hardware["activeReservation"]["id"]
150
- active_reservation_pk = hardware["activeReservation"]["pk"]
151
- reservation_number = hardware["activeReservation"][
152
- "reservationNumber"
153
- ]
154
- logger.debug("Active Reservation:")
155
- logger.info(f"Reservation Number: {reservation_number}")
156
- logger.debug(f"Node ID: {active_reservation_id}")
157
- logger.debug(f"PK: {active_reservation_pk}")
158
-
159
- logger.debug("Running pre provisioning steps for reservation.")
160
118
  self.primitive.provisioning.add_reservation_authorized_keys(
161
119
  reservation_id=active_reservation_id
162
120
  )
163
121
 
164
- if not active_reservation_id:
165
- self.primitive.hardware.check_in_http(
166
- is_available=True, is_online=True
167
- )
168
- logger.debug("Syncing children...")
169
- self.primitive.hardware._sync_children(hardware=hardware)
122
+ # we have an active reservation, check if we have JobRuns attached to it
123
+ if active_reservation_id is not None:
124
+ logger.info(f"Active Reservation ID: {active_reservation_id}")
125
+ job_run_data = (
126
+ self.primitive.reservations.get_job_run_for_reservation_id(
127
+ reservation_id=active_reservation_id
128
+ )
129
+ )
130
+ job_run_id = job_run_data.get("id")
170
131
 
171
- sleep_amount = 5
172
- logger.debug(
173
- f"No active reservation found... [sleeping {sleep_amount} seconds]"
174
- )
175
- sleep(sleep_amount)
176
- continue
177
- else:
178
- if (
179
- hardware["activeReservation"] is None
180
- and active_reservation_id is not None
181
- # and hardware["isAvailable"] NOTE: this condition was causing the CLI to get into a loop searching for job runs
182
- ):
183
- logger.debug("Previous Reservation is Complete:")
184
- logger.debug(f"Node ID: {active_reservation_id}")
185
- logger.debug(f"PK: {active_reservation_pk}")
186
- logger.debug(
187
- "Running cleanup provisioning steps for reservation."
132
+ # Golden state for normal reservation
133
+ if not job_run_id and active_reservation_id:
134
+ self.primitive.hardware.check_in_http(
135
+ is_available=False, is_online=True
188
136
  )
189
- self.primitive.provisioning.remove_reservation_authorized_keys(
190
- reservation_id=active_reservation_id
137
+ sleep_amount = 5
138
+ logger.info(
139
+ f"Waiting for Job Runs... [sleeping {sleep_amount} seconds]"
191
140
  )
192
- active_reservation_id = None
193
- active_reservation_pk = None
194
-
195
- # Third, see if the active reservation has any pending job runs
196
- job_runs_for_reservation = self.primitive.jobs.get_job_runs(
197
- status="pending", first=1, reservation_id=active_reservation_id
198
- )
199
-
200
- if (
201
- job_runs_for_reservation is None
202
- or job_runs_for_reservation.data is None
203
- ):
204
- logger.error("Error fetching job runs.")
141
+ sleep(sleep_amount)
142
+ continue
143
+
144
+ # job_run_data can come from 3 places:
145
+ # 1. an explicitly passed job_run_id
146
+ # 2. the previous reservation has an job_run_id (kill old PIDs)
147
+ # 3. the active reservation has an job_run_id (check status)
148
+ while job_run_id:
149
+ status_result = self.primitive.jobs.get_job_status(id=job_run_id)
150
+ get_status_timeout = 0
205
151
  sleep_amount = 5
206
- logger.debug(
207
- f"Error fetching job runs... [sleeping {sleep_amount} seconds]"
208
- )
209
- sleep(sleep_amount)
210
- continue
211
152
 
212
- pending_job_runs = [
213
- edge["node"]
214
- for edge in job_runs_for_reservation.data["jobRuns"]["edges"]
215
- ]
153
+ while get_status_timeout < MAX_GET_STATUS_TIMEOUT:
154
+ if not status_result or not status_result.data:
155
+ logger.error(
156
+ f"Error fetching job status for Job Run {job_run_id}. Retrying... [sleeping {sleep_amount} seconds]"
157
+ )
158
+ get_status_timeout += sleep_amount
159
+ sleep(sleep_amount)
160
+ continue
161
+ else:
162
+ break
216
163
 
217
- if not pending_job_runs:
218
- self.primitive.hardware.check_in_http(
219
- is_available=False, is_online=True
220
- )
221
- sleep_amount = 5
222
- logger.debug(
223
- f"Waiting for Job Runs... [sleeping {sleep_amount} seconds]"
224
- )
225
- sleep(sleep_amount)
226
- continue
227
-
228
- # If we did find a pending job run, check if it exists in the database
229
- # and create it if it doesn't.
230
- # This will trigger the agent to start the job run.
231
- job_run = pending_job_runs[0]
232
- if not JobRun.objects.filter_by(job_run_id=job_run["id"]).exists():
233
- JobRun.objects.create(job_run_id=job_run["id"], pid=None)
234
- logger.debug(f"Creating job run in database: {job_run['id']}")
164
+ if not status_result or not status_result.data:
165
+ raise P_CLI_101()
166
+
167
+ status_value = status_result.data["jobRun"]["status"]
168
+ parent_pid = status_result.data["jobRun"]["parentPid"]
169
+
170
+ if status_value == "completed":
171
+ logger.info(
172
+ f"Job run {job_run_id} is completed. Killing children if they exist."
173
+ )
174
+ if parent_pid is not None:
175
+ kill_process_and_children(pid=parent_pid)
176
+ status_value = None
177
+ job_run_id = None
178
+ else:
179
+ logger.info(
180
+ f"Job Run {job_run_id} with Status {status_value} with PID {parent_pid}. [sleeping {sleep_amount} seconds]"
181
+ )
182
+ sleep(sleep_amount)
183
+ continue
235
184
 
236
185
  except KeyboardInterrupt:
237
186
  logger.info("Stopping primitive monitor...")
@@ -176,3 +176,27 @@ class Reservations(BaseAction):
176
176
  )
177
177
 
178
178
  return reservation_result
179
+
180
+ @guard
181
+ def get_job_run_for_reservation_id(self, reservation_id: str) -> dict:
182
+ if not reservation_id:
183
+ logger.error("No reservation ID provided.")
184
+ return {}
185
+
186
+ job_runs_for_reservation = self.primitive.jobs.get_job_runs(
187
+ first=1,
188
+ reservation_id=reservation_id,
189
+ )
190
+
191
+ while job_runs_for_reservation is None or job_runs_for_reservation.data is None:
192
+ logger.error("Error fetching job runs.")
193
+ sleep_amount = 5
194
+ logger.info(f"Error fetching job runs... [sleeping {sleep_amount} seconds]")
195
+ sleep(sleep_amount)
196
+ continue
197
+
198
+ if not job_runs_for_reservation.data["jobRuns"]["edges"]:
199
+ logger.error("No job runs found for the given reservation ID.")
200
+ return {}
201
+
202
+ return job_runs_for_reservation.data["jobRuns"]["edges"][0]["node"]
@@ -10,3 +10,14 @@ class P_CLI_100(Exception):
10
10
 
11
11
  def __str__(self):
12
12
  return f"{self.codename}: {self.message}"
13
+
14
+
15
+ @dataclass
16
+ class P_CLI_101(Exception):
17
+ """Could Not Get Status for JobRun"""
18
+
19
+ codename: str = "P_CLI_101"
20
+ message: str = "Could Not Get Status for JobRun"
21
+
22
+ def __str__(self):
23
+ return f"{self.codename}: {self.message}"
@@ -0,0 +1,26 @@
1
+ import psutil
2
+ from loguru import logger
3
+
4
+
5
+ def kill_process_and_children(pid: int) -> bool:
6
+ """Kill a process and all its children."""
7
+ try:
8
+ try:
9
+ parent = psutil.Process(pid)
10
+ logger.info(f"Process PID {parent.pid} found.")
11
+ except psutil.NoSuchProcess:
12
+ logger.info("Process not found")
13
+ return False
14
+
15
+ children = parent.children(recursive=True)
16
+
17
+ for child in children:
18
+ logger.info(f"Killing child process {child.pid}...")
19
+ child.kill()
20
+
21
+ logger.info(f"Killing parent process {parent.pid}...")
22
+ parent.kill()
23
+ return True
24
+ except psutil.NoSuchProcess:
25
+ logger.warning(f"Process with PID {pid} not found.")
26
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primitive
3
- Version: 0.2.39
3
+ Version: 0.2.42
4
4
  Project-URL: Documentation, https://github.com//primitivecorp/primitive-cli#readme
5
5
  Project-URL: Issues, https://github.com//primitivecorp/primitive-cli/issues
6
6
  Project-URL: Source, https://github.com//primitivecorp/primitive-cli
@@ -26,7 +26,6 @@ Requires-Dist: psutil>=7.0.0
26
26
  Requires-Dist: pyyaml
27
27
  Requires-Dist: rich>=13.9.4
28
28
  Requires-Dist: speedtest-cli
29
- Requires-Dist: sqlalchemy>=2.0.40
30
29
  Description-Content-Type: text/markdown
31
30
 
32
31
  # primitive
@@ -1,11 +1,11 @@
1
- primitive/__about__.py,sha256=ZQkflfute36QGfeP2TGex_AxLvyIYgASuKv02D9bnfM,130
1
+ primitive/__about__.py,sha256=Vl72jTU-bFf1jFAZpJ-xozZlZLbpzZ7x5f0FzuwRdFc,130
2
2
  primitive/__init__.py,sha256=bwKdgggKNVssJFVPfKSxqFMz4IxSr54WWbmiZqTMPNI,106
3
3
  primitive/cli.py,sha256=g7EtHI9MATAB0qQu5w-WzbXtxz_8zu8z5E7sETmMkKU,2509
4
- primitive/client.py,sha256=h8WZVnQylVe0vbpuyC8YZHl2JyITSPC-1HbUcmrE5pc,3623
4
+ primitive/client.py,sha256=RMF46F89oK82gfZH6Bf0WZrhXPUu01pbieSO_Vcuoc4,3624
5
5
  primitive/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- primitive/agent/actions.py,sha256=vq4_CZy7Mn37jUg-JPuJlMLtcx4sMXp6tUNvXGZOOyQ,4128
7
- primitive/agent/commands.py,sha256=kqa-PGqmzS-APd4BSMAkX4l8SdK5N1PRQwq9S8jBjsw,390
8
- primitive/agent/runner.py,sha256=8q9xjQgcpXvQiUDdW9O6aS08lrJt9M_0_r9TZUAOdgw,15483
6
+ primitive/agent/actions.py,sha256=zQuxP7oGZ9PRIJLlJ5dhza-Vcg29gffRreCylDfYTtg,6365
7
+ primitive/agent/commands.py,sha256=o847pK7v7EWQGG67tky6a33qtwoutX6LZrP2FIS_NOk,388
8
+ primitive/agent/runner.py,sha256=wkI7ANC9ob99Fy5UWJAj__Ah7f7gwMgrBJ_LgpWZf4A,15638
9
9
  primitive/agent/uploader.py,sha256=ZzrzsajNBogwEC7mT6Ejy0h2Jd9axMYGzt9pbCvVMlk,3171
10
10
  primitive/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  primitive/auth/actions.py,sha256=9NIEXJ1BNJutJs6AMMSjMN_ziONUAUhY_xHwojYJCLA,942
@@ -18,9 +18,6 @@ primitive/daemons/commands.py,sha256=Xt4qFymNrDLdHJhRnEH_4Re-2xX6w1OT-chV9k7dFCs
18
18
  primitive/daemons/launch_agents.py,sha256=cCxsvCBFmGlKOiBINsKi_NxcozLC8yPw3w6pxqnz-qI,7803
19
19
  primitive/daemons/launch_service.py,sha256=iuklHeuEqadlf8U1n9xFg4ZG1EKdK2jyaPI-VTlpJ4I,7907
20
20
  primitive/daemons/ui.py,sha256=Af3OJWJ0jdGlb1nfA5yaGYdhBEqqpM8zP2U2vUQdCbw,1236
21
- primitive/db/base.py,sha256=mH7f2d_jiyxJSSx9Gk53QBXRa3LiKBsBjkFgvmtH1WA,83
22
- primitive/db/models.py,sha256=GfnJdAq4Tb68CI4BKAuJDZVqioGavveaAHbCPeLNngw,2840
23
- primitive/db/sqlite.py,sha256=ZtSi8Z0wiA5NEvgnP7udSwZnoUMLp0qMWfNZNC3htuI,2116
24
21
  primitive/exec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
22
  primitive/exec/actions.py,sha256=4d_TCjNDcVFoZ9Zw7ZuBa6hKMv2Xzm7_UX_8wcX1aSk,4124
26
23
  primitive/exec/commands.py,sha256=66LO2kkJC-ynNZQpUCXv4Ol15QoacdSZAHblePDcmLo,510
@@ -51,13 +48,13 @@ primitive/hardware/graphql/fragments.py,sha256=R1J75AQZNr6FSaaRzOn_IyCFIEaSb_YaU
51
48
  primitive/hardware/graphql/mutations.py,sha256=_4Hkbfik9Ron4T-meulu6T-9FR_BZjyPNwn745MPksU,1484
52
49
  primitive/hardware/graphql/queries.py,sha256=I86uLuOSjHSph11Y5MVCYko5Js7hoiEZ-cEoPTc4J-k,1392
53
50
  primitive/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
- primitive/jobs/actions.py,sha256=Fx2cPc1x09nRasOVtjhPjNRJ-jNoi3RJhXqC3verD9s,5444
51
+ primitive/jobs/actions.py,sha256=Fej-rpdfIiHjs0SEFY2ZcMn64AWHn6kD90RC5wtRM3Q,6531
55
52
  primitive/jobs/commands.py,sha256=MxPCkBEYW_eLNqgCRYeyj7ZcLOFAWfpVZlqDR2Y_S0o,830
56
53
  primitive/jobs/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
- primitive/jobs/graphql/fragments.py,sha256=flBi4REkfPX3sao22McmjclK-lFsqiXQa21YKmTbxu8,653
54
+ primitive/jobs/graphql/fragments.py,sha256=Ldhsc-NFHZGY8_1FjO2uZIdOL5UUGZjsldHea_GLJ_g,667
58
55
  primitive/jobs/graphql/mutations.py,sha256=8ASvCmwQh7cMeeiykOdYaYVryG8FRIuVF6v_J8JJZuw,219
59
56
  primitive/jobs/graphql/queries.py,sha256=ZxNmm-WovytbggNuKRnwa0kc26T34_0yhqkoqx-2uj0,1736
60
- primitive/monitor/actions.py,sha256=l8J8h_GyCmFqmf6hRL34m-tc04cCW76_VYlsCCDk5IE,11120
57
+ primitive/monitor/actions.py,sha256=7T205EU2ULvGQmiAJy99qG5vXx_7xVpjV_P6X6Osso8,9295
61
58
  primitive/monitor/commands.py,sha256=VDlEL_Qpm_ysHxug7VpI0cVAZ0ny6AS91Y58D7F1zkU,409
62
59
  primitive/organizations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
60
  primitive/organizations/actions.py,sha256=kVHOhG1oS2sI5p8uldSo5L-RUZsnG36eaulVuKLyZ-M,1863
@@ -78,7 +75,7 @@ primitive/provisioning/actions.py,sha256=IYZYAbtomtZtlkqDaBxx4e7PFKGkRNqek_tABH6
78
75
  primitive/provisioning/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
76
  primitive/provisioning/graphql/queries.py,sha256=cBtuKa6shoatYZfKSnQoPJP6B8g8y3QhFqJ_pkvMcG0,134
80
77
  primitive/reservations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
- primitive/reservations/actions.py,sha256=FiodRTVUgGgFfoksnN9W0XNdGTd2AxPJTfUrZbmQ0_g,6179
78
+ primitive/reservations/actions.py,sha256=AYAWJmUItXTb7n1zyFt0Oov09rpBrLy6yIYHPpg69gM,7075
82
79
  primitive/reservations/commands.py,sha256=LFRoV59QGgWIjBdrGjJdffHugg8TLe0Fwlcyu_JaTkk,2369
83
80
  primitive/reservations/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
81
  primitive/reservations/graphql/fragments.py,sha256=h3edd0Es38SNldhHiRU3hAJNYwrUIJMWxiP8av3BfrI,431
@@ -91,14 +88,15 @@ primitive/utils/cache.py,sha256=FHGmVWYLJFQOazpXXcEwI0YJEZbdkgG39nOLdOv6VNk,1575
91
88
  primitive/utils/chunk_size.py,sha256=PAuVuirUTA9oRXyjo1c6MWxo31WVBRkWMuWw-AS58Bw,2914
92
89
  primitive/utils/config.py,sha256=DlFM5Nglo22WPtbpZSVtH7NX-PTMaKYlcrUE7GPRG4c,1058
93
90
  primitive/utils/daemons.py,sha256=mSoSHitiGfS4KYAEK9sKsiv_YcACHKgY3qISnDpUUIE,1086
94
- primitive/utils/exceptions.py,sha256=DrYHTcCAJGC7cCUwOx_FmdlVLWRdpzvDvpLb82heppE,311
91
+ primitive/utils/exceptions.py,sha256=KSAR2zqWEjQhqb6OAtaMUvYejKPETdBVRRRIKzy90A8,554
95
92
  primitive/utils/logging.py,sha256=vpwu-hByZC1BgJfUi6iSfAxzCobP_zg9-99EUf80KtY,1132
96
93
  primitive/utils/memory_size.py,sha256=4xfha21kW82nFvOTtDFx9Jk2ZQoEhkfXii-PGNTpIUk,3058
97
94
  primitive/utils/printer.py,sha256=f1XUpqi5dkTL3GWvYRUGlSwtj2IxU1q745T4Fxo7Tn4,370
95
+ primitive/utils/psutil.py,sha256=xa7ef435UL37jyjmUPbEqCO2ayQMpCs0HCrxVEvLcuM,763
98
96
  primitive/utils/shell.py,sha256=Z4zxmOaSyGCrS0D6I436iQci-ewHLt4UxVg1CD9Serc,2171
99
97
  primitive/utils/text.py,sha256=XiESMnlhjQ534xE2hMNf08WehE1SKaYFRNih0MmnK0k,829
100
- primitive-0.2.39.dist-info/METADATA,sha256=lJU7WTvdfIpoE4cpyEAtBRkQRw0tzhXSW6rjBNNbJEU,3569
101
- primitive-0.2.39.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
102
- primitive-0.2.39.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
103
- primitive-0.2.39.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
104
- primitive-0.2.39.dist-info/RECORD,,
98
+ primitive-0.2.42.dist-info/METADATA,sha256=9VdknXdkOAgcn7ckBzk3VYumF1wkmO7iE1nExBibq8w,3535
99
+ primitive-0.2.42.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
100
+ primitive-0.2.42.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
101
+ primitive-0.2.42.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
102
+ primitive-0.2.42.dist-info/RECORD,,
primitive/db/base.py DELETED
@@ -1,5 +0,0 @@
1
- from sqlalchemy.orm import DeclarativeBase
2
-
3
-
4
- class Base(DeclarativeBase):
5
- pass
primitive/db/models.py DELETED
@@ -1,88 +0,0 @@
1
- from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar, Union
2
-
3
- from sqlalchemy import Column, Integer, String
4
- from sqlalchemy.orm import Mapped, Query, mapped_column
5
-
6
- from .base import Base
7
- from .sqlite import Session
8
-
9
- T = TypeVar("T", bound="Base")
10
-
11
-
12
- class Manager(Generic[T]):
13
- def __init__(self, model_cls_lambda: Callable[[], Type[T]]) -> None:
14
- self.model_cls_lambda = model_cls_lambda
15
- self.filters: Dict[str, Any] = {}
16
-
17
- def create(self, **kwargs) -> T:
18
- with Session() as session:
19
- model = self.model_cls_lambda()
20
- obj = model(**kwargs)
21
- session.add(obj)
22
- session.commit()
23
- session.refresh(obj)
24
- return obj
25
-
26
- def filter_by(self, **kwargs) -> "Manager[T]":
27
- self.filters = kwargs
28
- return self
29
-
30
- def exists(self) -> bool:
31
- with Session() as session:
32
- model = self.model_cls_lambda()
33
- query = session.query(model)
34
- query.filter_by(**self.filters)
35
- self.filters.clear()
36
- return query.count() > 0
37
-
38
- def all(self) -> List[T]:
39
- with Session() as session:
40
- model = self.model_cls_lambda()
41
- query = session.query(model)
42
- query.filter_by(**self.filters)
43
- self.filters.clear()
44
- return query.all()
45
-
46
- def first(self) -> Union[T, None]:
47
- with Session() as session:
48
- model = self.model_cls_lambda()
49
- query = session.query(model)
50
- query.filter_by(**self.filters)
51
- self.filters.clear()
52
- return query.first()
53
-
54
- def update(self, update: Dict[Any, Any]) -> Query[T]:
55
- with Session() as session:
56
- model = self.model_cls_lambda()
57
- query = session.query(model).filter_by(**self.filters)
58
-
59
- if query.count() > 0:
60
- query.update(update)
61
- session.commit()
62
- return query
63
- else:
64
- raise ValueError(f"Update failed, {model.__name__} not found")
65
-
66
- def delete(self) -> None:
67
- with Session() as session:
68
- model = self.model_cls_lambda()
69
- query = session.query(model).filter_by(**self.filters)
70
-
71
- if query.count() > 0:
72
- query.delete()
73
- session.commit()
74
- else:
75
- raise ValueError(f"Delete failed, {model.__name__} not found")
76
-
77
-
78
- class JobRun(Base):
79
- __tablename__ = "JobRun"
80
-
81
- id = Column(Integer, primary_key=True)
82
- job_run_id: Mapped[str] = mapped_column(String, nullable=False)
83
- pid: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
84
-
85
- objects: Manager["JobRun"] = Manager(lambda: JobRun)
86
-
87
- def __repr__(self):
88
- return f"<JobRun(id={self.id} job_run_id={self.job_run_id}, pid={self.pid})>"
primitive/db/sqlite.py DELETED
@@ -1,70 +0,0 @@
1
- from pathlib import Path
2
- from time import sleep
3
-
4
- from loguru import logger
5
- from sqlalchemy import Engine, create_engine, inspect
6
- from sqlalchemy.orm import Session as SQLAlchemySession
7
-
8
- from primitive.db.base import Base
9
- from primitive.utils.cache import get_cache_dir
10
-
11
-
12
- def init() -> None:
13
- db_path: Path = get_cache_dir() / "primitive.sqlite3"
14
-
15
- # Drop DB existing database if it exists
16
- # if db_path.exists():
17
- # logger.warning(f"Deleting existing SQLite database at {db_path}")
18
- # db_path.unlink()
19
- if db_path.exists():
20
- return
21
-
22
- logger.info(f"Initializing SQLite database at {db_path}")
23
- engine = create_engine(f"sqlite:///{db_path}", echo=False)
24
- Base.metadata.create_all(engine)
25
-
26
-
27
- def engine() -> Engine:
28
- db_path: Path = get_cache_dir() / "primitive.sqlite3"
29
- return create_engine(f"sqlite:///{db_path}", echo=False)
30
-
31
-
32
- def wait_for_db() -> None:
33
- # Wait for the database to be created
34
- db_path: Path = get_cache_dir() / "primitive.sqlite3"
35
-
36
- max_tries = 60
37
- current_try = 1
38
- while not db_path.exists() and current_try <= max_tries:
39
- logger.debug(
40
- f"Waiting for SQLite database to be created... [{current_try} / {max_tries}]"
41
- )
42
- sleep(1)
43
- current_try += 1
44
- continue
45
- if current_try > max_tries:
46
- message = f"SQLite database was not created after {max_tries} tries. Exiting..."
47
- logger.error(message)
48
- raise RuntimeError(message)
49
-
50
- # check if table exists
51
- max_tries = 60
52
- current_try = 1
53
- while not inspect(engine()).has_table("JobRun") and current_try <= max_tries:
54
- logger.error("SQLite database is not ready.")
55
- sleep(1)
56
- current_try += 1
57
- continue
58
- if current_try > max_tries:
59
- message = (
60
- f"Database tables were not created after {max_tries} tries. Exiting..."
61
- )
62
- logger.error(message)
63
- raise RuntimeError(message)
64
- logger.debug("SQLite database is ready.")
65
-
66
-
67
- def Session() -> SQLAlchemySession:
68
- from sqlalchemy.orm import sessionmaker
69
-
70
- return sessionmaker(bind=engine())()