-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
602 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
from __future__ import annotations | ||
|
||
from datetime import datetime, timedelta | ||
from typing import TYPE_CHECKING, Any, Optional | ||
|
||
import pause | ||
from sqlalchemy import JSON, DateTime | ||
from sqlalchemy.ext.asyncio import async_scoped_session | ||
from sqlalchemy.ext.hybrid import hybrid_property | ||
from sqlalchemy.orm import Mapped, mapped_column, relationship | ||
from sqlalchemy.schema import ForeignKey | ||
|
||
from ..common.enums import LevelEnum | ||
from .base import Base | ||
from .campaign import Campaign | ||
from .dbid import DbId | ||
from .element import ElementMixin | ||
from .group import Group | ||
from .job import Job | ||
from .node import NodeMixin | ||
from .step import Step | ||
|
||
if TYPE_CHECKING: | ||
pass | ||
|
||
|
||
class Queue(Base, NodeMixin): | ||
"""Database table to implement processing queue""" | ||
|
||
__tablename__ = "queue" | ||
|
||
id: Mapped[int] = mapped_column(primary_key=True) | ||
time_created: Mapped[DateTime] = mapped_column() | ||
time_updated: Mapped[DateTime] = mapped_column() | ||
time_finished: Mapped[DateTime | None] = mapped_column(default=None) | ||
interval: Mapped[float] = mapped_column(default=300.0) | ||
options: Mapped[Optional[dict | list]] = mapped_column(type_=JSON) | ||
|
||
element_level: Mapped[LevelEnum] = mapped_column() | ||
element_id: Mapped[int] = mapped_column() | ||
c_id: Mapped[int | None] = mapped_column(ForeignKey("campaign.id", ondelete="CASCADE"), index=True) | ||
s_id: Mapped[int | None] = mapped_column(ForeignKey("step.id", ondelete="CASCADE"), index=True) | ||
g_id: Mapped[int | None] = mapped_column(ForeignKey("group.id", ondelete="CASCADE"), index=True) | ||
j_id: Mapped[int | None] = mapped_column(ForeignKey("job.id", ondelete="CASCADE"), index=True) | ||
|
||
c_: Mapped["Campaign"] = relationship("Campaign", viewonly=True) | ||
s_: Mapped["Step"] = relationship("Step", viewonly=True) | ||
g_: Mapped["Group"] = relationship("Group", viewonly=True) | ||
j_: Mapped["Job"] = relationship("Job", viewonly=True) | ||
|
||
@hybrid_property | ||
def element_db_id(self) -> DbId: | ||
"""Returns DbId""" | ||
return DbId(self.element_level, self.element_id) | ||
|
||
async def get_element( | ||
self, | ||
session: async_scoped_session, | ||
) -> ElementMixin: | ||
"""Get the parent `Element` | ||
Parameters | ||
---------- | ||
session : async_scoped_session | ||
DB session manager | ||
Returns | ||
------- | ||
element : ElementMixin | ||
Requested Parent Element | ||
""" | ||
async with session.begin_nested(): | ||
element: ElementMixin | None = None | ||
if self.element_level == LevelEnum.campaign: | ||
await session.refresh(self, attribute_names=["c_"]) | ||
element = self.c_ | ||
elif self.element_level == LevelEnum.step: | ||
await session.refresh(self, attribute_names=["s_"]) | ||
element = self.s_ | ||
elif self.element_level == LevelEnum.group: | ||
await session.refresh(self, attribute_names=["g_"]) | ||
element = self.g_ | ||
elif self.element_level == LevelEnum.job: | ||
await session.refresh(self, attribute_names=["j_"]) | ||
element = self.j_ | ||
else: | ||
raise ValueError(f"Bad level for script: {self.element_level}") | ||
if TYPE_CHECKING: | ||
assert isinstance(element, ElementMixin) | ||
return element | ||
|
||
@classmethod | ||
async def get_create_kwargs( | ||
cls, | ||
session: async_scoped_session, | ||
**kwargs: Any, | ||
) -> dict: | ||
element_name = kwargs["element_name"] | ||
element_level = kwargs["element_level"] | ||
|
||
now = datetime.now() | ||
ret_dict = { | ||
"element_level": element_level, | ||
"time_created": now, | ||
"time_updated": now, | ||
"options": kwargs.get("options", {}), | ||
} | ||
|
||
if element_level == LevelEnum.campaign: | ||
element = await Campaign.get_row_by_fullname(session, element_name) | ||
ret_dict["c_id"] = element.id | ||
elif element_level == LevelEnum.step: | ||
element = await Step.get_row_by_fullname(session, element_name) | ||
ret_dict["s_id"] = element.id | ||
elif element_level == LevelEnum.group: | ||
element = await Group.get_row_by_fullname(session, element_name) | ||
ret_dict["g_id"] = element.id | ||
elif element_level == LevelEnum.job: | ||
element = await Job.get_row_by_fullname(session, element_name) | ||
ret_dict["j_id"] = element.id | ||
else: | ||
raise ValueError(f"Bad level for script: {element_level}") | ||
ret_dict["element_id"] = element.id | ||
return ret_dict | ||
|
||
def waiting( | ||
self, | ||
) -> bool: | ||
"""Check if this the Queue Element is done waiting | ||
Returns | ||
------- | ||
done: bool | ||
Returns True if still waiting | ||
""" | ||
delta_t = timedelta(seconds=self.interval) | ||
next_check = self.time_updated + delta_t | ||
now = datetime.now() | ||
return now < next_check | ||
|
||
def pause_until_next_check( | ||
self, | ||
) -> None: | ||
"""Sleep until the next time check""" | ||
delta_t = timedelta(seconds=self.interval) | ||
next_check = self.time_updated + delta_t | ||
now = datetime.now() | ||
if now < next_check: | ||
pause.until(next_check) | ||
|
||
async def _process_and_update( | ||
self, | ||
session: async_scoped_session, | ||
) -> bool: | ||
element = await self.get_element() | ||
if not element.status.is_processable_element(): | ||
return False | ||
|
||
status = await element.process(session, **self.options) | ||
now = datetime.now() | ||
update_dict = {"time_updated": now} | ||
if status.is_successful_element(): | ||
update_dict.update(time_finished=now) | ||
|
||
await self.update_values(session, **update_dict) | ||
return element.status.is_processable_element() | ||
|
||
async def process_element( | ||
self, | ||
session: async_scoped_session, | ||
) -> bool: | ||
"""Process associated element""" | ||
if self.waiting(): | ||
return True | ||
return await self._process_and_update(session) | ||
|
||
async def process_element_loop( | ||
self, | ||
session: async_scoped_session, | ||
) -> None: | ||
can_continue = True | ||
while can_continue: | ||
self.pause_until_next_check() | ||
can_continue = await self._process_and_update(session) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
from __future__ import annotations | ||
|
||
import os | ||
from typing import TYPE_CHECKING, Any, Optional | ||
|
||
from sqlalchemy.ext.asyncio import async_scoped_session | ||
from sqlalchemy.orm import Mapped, mapped_column, relationship | ||
from sqlalchemy.schema import ForeignKey | ||
|
||
from .base import Base | ||
from .row import RowMixin | ||
from .specification import Specification | ||
|
||
|
||
class ScriptTemplate(Base, RowMixin): | ||
"""Database table to manage script templates | ||
A 'ScriptTemplate' is a template that gets used to create a bash script | ||
""" | ||
|
||
__tablename__ = "spec_block" | ||
|
||
id: Mapped[int] = mapped_column(primary_key=True) | ||
spec_id: Mapped[int] = mapped_column(ForeignKey("specification.id", ondelete="CASCADE"), index=True) | ||
name: Mapped[str] = mapped_column(index=True) | ||
fullname: Mapped[str] = mapped_column(unique=True) | ||
data: Mapped[Optional[str]] = mapped_column() | ||
|
||
spec_: Mapped["Specification"] = relationship("Specification", viewonly=True) | ||
|
||
def __repr__(self) -> str: | ||
return f"ScriptTemplate {self.id}: {self.fullname} {self.data}" | ||
|
||
@classmethod | ||
async def get_create_kwargs( | ||
cls, | ||
session: async_scoped_session, | ||
**kwargs: Any, | ||
) -> dict: | ||
spec_name = kwargs["spec_name"] | ||
spec = await Specification.get_row_by_fullname(session, spec_name) | ||
name = kwargs["name"] | ||
|
||
ret_dict = { | ||
"spec_id": spec.id, | ||
"name": name, | ||
"fullname": f"{spec_name}#{name}", | ||
"data": kwargs.get("data", None), | ||
} | ||
return ret_dict | ||
|
||
@classmethod | ||
async def load( | ||
cls, | ||
session: async_scoped_session, | ||
spec_name: str, | ||
file_path: str, | ||
) -> ScriptTemplate: | ||
"""Load a ScriptTemplate from a file | ||
Parameters | ||
---------- | ||
session : async_scoped_session | ||
DB session manager | ||
spec_name: str, | ||
Name for the specification | ||
file_path | ||
Path to the file | ||
Returns | ||
------- | ||
script_template : `ScriptTemplate` | ||
Newly created `ScriptTemplate` | ||
""" | ||
full_file_path = os.path.abspath(os.path.expandvars(file_path)) | ||
with open(full_file_path, "r") as fin: | ||
data = fin.read() | ||
|
||
new_row = cls.create_row(session, spec_name=spec_name, data=data) | ||
if TYPE_CHECKING: | ||
assert isinstance(new_row, ScriptTemplate) | ||
return new_row |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.