Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add order status service #2965

Merged
merged 40 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
81b7a25
Add query param
seallard Feb 20, 2024
1cced92
Restructure
seallard Feb 20, 2024
e4a3feb
Retrieve summaries from trailblazer
seallard Feb 20, 2024
dcbd8a7
Clean up response dtos
seallard Feb 20, 2024
feef77e
Formatting
seallard Feb 20, 2024
8b6b17d
Fix model
seallard Feb 21, 2024
102a3eb
Move order service to ext module
seallard Feb 21, 2024
c8e5fa9
Set env vars
seallard Feb 21, 2024
40c0fe2
Fix tests
seallard Feb 21, 2024
9f1d178
Restore old test
seallard Feb 21, 2024
66df963
Fix dockerfile
seallard Feb 21, 2024
f9bfcf9
Merge
seallard Feb 21, 2024
6c77a94
Formatting
seallard Feb 21, 2024
4898c81
Formatting
seallard Feb 21, 2024
4d5de3f
Fix optional parameters
seallard Feb 21, 2024
5693e60
Fix query
seallard Feb 21, 2024
06c504a
Fix type
seallard Feb 21, 2024
dcf6450
Change param
seallard Feb 23, 2024
73046ae
Merge master
seallard Feb 23, 2024
e937731
Make total count the case count
seallard Feb 23, 2024
c63bf43
Extract summary service
seallard Feb 23, 2024
831289d
Fix import
seallard Feb 23, 2024
1418dec
Naming
seallard Feb 26, 2024
c18a59f
wip
seallard Feb 26, 2024
d74caf9
Add test
seallard Feb 26, 2024
1262f3f
wip
seallard Feb 26, 2024
9824272
Add case status summaries
seallard Feb 26, 2024
da54c47
Fix comments
seallard Feb 27, 2024
cdca79c
Fix comments
seallard Feb 27, 2024
fa15766
Merge branch 'master' into add-order-summary
seallard Feb 27, 2024
f7a4289
Naming
seallard Feb 27, 2024
b4df1eb
Fix endpoint name
seallard Feb 27, 2024
9787703
Fix trailblazer call
seallard Feb 27, 2024
5ba1b2c
Fix logic for case status
seallard Feb 28, 2024
c86c245
Fix missing import
seallard Feb 28, 2024
a80640b
Merge master
seallard Feb 28, 2024
6058142
Fix naming
seallard Feb 28, 2024
d52c6a0
Cleanup
seallard Feb 28, 2024
1fd4aaf
Fix summary
seallard Feb 28, 2024
336479a
Fix typing
seallard Feb 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ ENV OSTICKET_TIMEOUT="1"
ENV GOOGLE_OAUTH_CLIENT_ID="1"
ENV GOOGLE_OAUTH_CLIENT_SECRET="1"

ENV TRAILBLAZER_HOST="host"
ENV TRAILBLAZER_SERVICE_ACCOUNT="service_account"
ENV TRAILBLAZER_SERVICE_ACCOUNT_AUTH_FILE="auth_file"


WORKDIR /home/src/app
COPY pyproject.toml poetry.lock ./
Expand Down
8 changes: 8 additions & 0 deletions cg/apps/tb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from google.auth.crypt import RSASigner

from cg.apps.tb.dto.create_job_request import CreateJobRequest
from cg.apps.tb.dto.summary_response import SummariesResponse, AnalysisSummary
from cg.apps.tb.models import AnalysesResponse, TrailblazerAnalysis
from cg.constants import Workflow
from cg.constants.constants import APIMethods, FileFormat, JobType, WorkflowManager
Expand Down Expand Up @@ -170,3 +171,10 @@ def add_upload_job_to_analysis(self, analysis_id: int, slurm_id: int) -> None:
request_body=request_body,
method=APIMethods.POST,
)

def get_summaries(self, order_ids: list[int]) -> list[AnalysisSummary]:
orders_param = "orderIds=" + ",".join(map(str, order_ids))
endpoint = f"summary?{orders_param}"
response = self.query_trailblazer(command=endpoint, request_body={}, method=APIMethods.GET)
response_data = SummariesResponse.model_validate(response)
return response_data.summaries
13 changes: 13 additions & 0 deletions cg/apps/tb/dto/summary_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from pydantic import BaseModel


class AnalysisSummary(BaseModel):
order_id: int
delivered: int | None = None
running: int | None = None
cancelled: int | None = None
failed: int | None = None


class SummariesResponse(BaseModel):
summaries: list[AnalysisSummary]
13 changes: 5 additions & 8 deletions cg/server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
CaseNotFoundError,
OrderError,
OrderFormError,
OrderNotFoundError,
TicketCreationError,
)
from cg.io.controller import WriteStream
Expand All @@ -36,9 +35,10 @@
from cg.server.dto.delivery_message_response import DeliveryMessageResponse
from cg.server.dto.orders.orders_request import OrdersRequest
from cg.server.dto.orders.orders_response import Order, OrdersResponse
from cg.server.ext import db, lims, osticket
from cg.server.ext import db, lims, osticket, order_service
from cg.server.utils import parse_orders_request
from cg.services.delivery_message.delivery_message_service import DeliveryMessageService
from cg.services.orders.order_service import OrderService
from cg.services.orders.order_service.exceptions import OrderNotFoundError
from cg.store.models import (
Analysis,
Application,
Expand Down Expand Up @@ -135,7 +135,6 @@ def submit_order(order_type):
user_name=g.current_user.name,
user_mail=g.current_user.email,
)
order_service = OrderService(db)
order_service.create_order(order_in)

except ( # user misbehaviour
Expand Down Expand Up @@ -481,16 +480,14 @@ def get_application_pipeline_limitations(tag: str):
@BLUEPRINT.route("/orders")
def get_orders():
"""Return the latest orders."""
orders_request: OrdersRequest = OrdersRequest.model_validate(request.args.to_dict())
order_service = OrderService(db)
response: OrdersResponse = order_service.get_orders(orders_request)
data: OrdersRequest = parse_orders_request(request)
response: OrdersResponse = order_service.get_orders(data)
return make_response(response.model_dump())


@BLUEPRINT.route("/orders/<order_id>")
def get_order(order_id: int):
"""Return an order."""
order_service = OrderService(db)
try:
response: Order = order_service.get_order(order_id)
response_dict: dict = response.model_dump()
Expand Down
1 change: 1 addition & 0 deletions cg/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def _configure_extensions(app: Flask):
ext.csrf.init_app(app)
ext.db.init_app(app)
ext.lims.init_app(app)
ext.analysis_client.init_app(app)
if app.config["OSTICKET_API_KEY"]:
ext.osticket.init_app(app)
ext.admin.init_app(app, index_view=AdminIndexView(endpoint="admin"))
Expand Down
7 changes: 7 additions & 0 deletions cg/server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@

# invoice
TOTAL_PRICE_THRESHOLD = 750000

# trailblazer
TRAILBLAZER_SERVICE_ACCOUNT = os.environ["TRAILBLAZER_SERVICE_ACCOUNT"] or "service"
TRAILBLAZER_SERVICE_ACCOUNT_AUTH_FILE = (
os.environ["TRAILBLAZER_SERVICE_ACCOUNT_AUTH_FILE"] or "service_file"
)
TRAILBLAZER_HOST = os.environ["TRAILBLAZER_HOST"] or "host"
3 changes: 2 additions & 1 deletion cg/server/dto/orders/orders_request.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field


class OrdersRequest(BaseModel):
limit: int | None = None
workflow: str | None = None
include_summary: bool = Field(default=False, alias="includeSummary")
2 changes: 2 additions & 0 deletions cg/server/dto/orders/orders_response.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pydantic import BaseModel

from cg.constants import Workflow
from cg.services.orders.order_status_service.dto.order_status_summary import OrderSummary


class Order(BaseModel):
Expand All @@ -9,6 +10,7 @@ class Order(BaseModel):
order_date: str
order_id: int
workflow: Workflow
summary: OrderSummary | None = None


class OrdersResponse(BaseModel):
Expand Down
25 changes: 25 additions & 0 deletions cg/server/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@

from cg.apps.lims import LimsAPI
from cg.apps.osticket import OsTicket
from cg.services.orders.order_status_service.order_status_service import OrderStatusService
from cg.store.database import initialize_database
from cg.store.store import Store
from cg.apps.tb.api import TrailblazerAPI
from cg.services.orders.order_service.order_service import OrderService


class FlaskLims(LimsAPI):
Expand Down Expand Up @@ -45,10 +48,32 @@ def default(self, obj):
return super().default(obj)


class AnalysisClient(TrailblazerAPI):
def __init__(self, app=None):
if app:
self.init_app(app)

def init_app(self, app):
service_account: str = app.config["TRAILBLAZER_SERVICE_ACCOUNT"]
service_account_auth_file: str = app.config["TRAILBLAZER_SERVICE_ACCOUNT_AUTH_FILE"]
host: str = app.config["TRAILBLAZER_HOST"]
config = {
"trailblazer": {
"service_account": service_account,
"service_account_auth_file": service_account_auth_file,
"host": host,
}
}
super(AnalysisClient, self).__init__(config)


cors = CORS(resources={r"/api/*": {"origins": "*"}}, supports_credentials=True)
csrf = CSRFProtect()
db = FlaskStore()

admin = Admin(name="Clinical Genomics")
lims = FlaskLims()
osticket = OsTicket()
analysis_client = AnalysisClient()
summary_service = OrderStatusService(store=db, analysis_client=analysis_client)
order_service = OrderService(store=db, status_service=summary_service)
8 changes: 8 additions & 0 deletions cg/server/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from requests import Request

from cg.server.dto.orders.orders_request import OrdersRequest


def parse_orders_request(request: Request) -> OrdersRequest:
query_params: dict = request.args.to_dict()
return OrdersRequest.model_validate(query_params)
4 changes: 0 additions & 4 deletions cg/services/fastq_file_service/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,3 @@ class FastqServiceError(Exception):

class ConcatenationError(FastqServiceError):
pass


class InvalidFastqDirectory(FastqServiceError):
pass
Empty file.
6 changes: 6 additions & 0 deletions cg/services/orders/order_service/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class OrderServiceError(Exception):
pass


class OrderNotFoundError(OrderServiceError):
pass
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
from cg.exc import OrderNotFoundError
from cg.models.orders.order import OrderIn
from cg.server.dto.orders.orders_request import OrdersRequest
from cg.server.dto.orders.orders_response import Order as OrderResponse
from cg.server.dto.orders.orders_response import OrdersResponse
from cg.services.orders.utils import create_order_response, create_orders_response
from cg.services.orders.order_service.exceptions import OrderNotFoundError
from cg.services.orders.order_service.utils import (
create_order_response,
create_orders_response,
)
from cg.services.orders.order_status_service import OrderStatusService
from cg.services.orders.order_status_service.dto.order_status_summary import OrderSummary
from cg.store.models import Order
from cg.store.store import Store


class OrderService:
def __init__(self, store: Store) -> None:
def __init__(self, store: Store, status_service: OrderStatusService) -> None:
self.store = store
self.summary_service = status_service

def get_order(self, order_id: int) -> OrderResponse:
order: Order | None = self.store.get_order_by_id(order_id)
Expand All @@ -19,14 +25,14 @@ def get_order(self, order_id: int) -> OrderResponse:
return create_order_response(order)

def get_orders(self, orders_request: OrdersRequest) -> OrdersResponse:
orders: list[Order] = self._get_orders(orders_request)
return create_orders_response(orders)
orders: list[Order] = self.store.get_orders(orders_request)

def _get_orders(self, orders_request: OrdersRequest) -> list[Order]:
"""Returns a list of entries in the table Order."""
return self.store.get_orders_by_workflow(
workflow=orders_request.workflow, limit=orders_request.limit
)
summaries: list[OrderSummary] = []
if orders_request.include_summary:
order_ids: list[int] = [order.id for order in orders]
summaries = self.summary_service.get_status_summaries(order_ids)

return create_orders_response(orders=orders, summaries=summaries)

seallard marked this conversation as resolved.
Show resolved Hide resolved
def create_order(self, order_data: OrderIn) -> OrderResponse:
"""Creates an order and links it to the given cases."""
Expand Down
28 changes: 28 additions & 0 deletions cg/services/orders/order_service/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from cg.server.dto.orders.orders_response import Order, OrderSummary, OrdersResponse
from cg.store.models import Order as DatabaseOrder


def create_order_response(order: DatabaseOrder) -> Order:
return Order(
customer_id=order.customer.internal_id,
ticket_id=order.ticket_id,
order_date=str(order.order_date.date()),
order_id=order.id,
workflow=order.workflow,
)


def create_orders_response(
orders: list[DatabaseOrder], summaries: list[OrderSummary]
) -> OrdersResponse:
orders: list[Order] = [create_order_response(order) for order in orders]
_add_summaries(orders=orders, summaries=summaries)
return OrdersResponse(orders=orders)


def _add_summaries(orders: list[Order], summaries: list[OrderSummary]) -> list[Order]:
order_map = {order.order_id: order for order in orders}
for summary in summaries:
order = order_map[summary.order_id]
order.summary = summary
seallard marked this conversation as resolved.
Show resolved Hide resolved
return orders
seallard marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions cg/services/orders/order_status_service/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from cg.services.orders.order_status_service.order_status_service import OrderStatusService
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pydantic import BaseModel


class OrderSummary(BaseModel):
order_id: int
total: int
delivered: int | None = None
running: int | None = None
cancelled: int | None = None
failed: int | None = None
in_sequencing: int
in_lab_preparation: int
20 changes: 20 additions & 0 deletions cg/services/orders/order_status_service/order_status_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from cg.apps.tb.api import TrailblazerAPI
from cg.apps.tb.dto.summary_response import AnalysisSummary
from cg.services.orders.order_status_service.dto.order_status_summary import OrderSummary
from cg.services.orders.order_status_service.utils import (
create_summaries,
create_summaries,
)
from cg.store.models import Order
from cg.store.store import Store


class OrderStatusService:
def __init__(self, analysis_client: TrailblazerAPI, store: Store) -> None:
self.analysis_client = analysis_client
self.store = store

def get_status_summaries(self, order_ids: list[int]) -> list[OrderSummary]:
seallard marked this conversation as resolved.
Show resolved Hide resolved
orders: list[Order] = self.store.get_orders_by_ids(order_ids)
analysis_summaries: list[AnalysisSummary] = self.analysis_client.get_summaries(order_ids)
return create_summaries(orders=orders, analysis_summaries=analysis_summaries)
67 changes: 67 additions & 0 deletions cg/services/orders/order_status_service/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from enum import Enum
from cg.apps.tb.dto.summary_response import AnalysisSummary
from cg.services.orders.order_status_service.dto.order_status_summary import OrderSummary
from cg.store.models import Case, Order


def create_summaries(
orders: list[Order], analysis_summaries: list[AnalysisSummary]
) -> list[OrderSummary]:
summaries: list[OrderSummary] = []
for order in orders:
summary: OrderSummary = create_order_summary(order)
summaries.append(summary)
add_analysis_summaries(order_summaries=summaries, analysis_summaries=analysis_summaries)
return summaries


def add_analysis_summaries(
order_summaries: list[OrderSummary],
analysis_summaries: list[AnalysisSummary],
) -> None:
order_summary_map = {summary.order_id: summary for summary in order_summaries}
for analysis_summary in analysis_summaries:
order_summary = order_summary_map[analysis_summary.order_id]
order_summary.delivered = analysis_summary.delivered
order_summary.running = analysis_summary.running
order_summary.cancelled = analysis_summary.cancelled
order_summary.failed = analysis_summary.failed
seallard marked this conversation as resolved.
Show resolved Hide resolved


class CaseStatus(Enum):
SEQUENCING = 1
LAB_PREPARATION = 2
OTHER = 3


def create_order_summary(order: Order) -> OrderSummary:
in_sequencing: int = 0
in_preparation: int = 0

for case in order.cases:
status: CaseStatus = get_case_status(case)
if status == CaseStatus.SEQUENCING:
in_sequencing += 1
if status == CaseStatus.LAB_PREPARATION:
in_preparation += 1

return OrderSummary(
order_id=order.id,
total=len(order.cases),
in_sequencing=in_sequencing,
in_lab_preparation=in_preparation,
)


def get_case_status(case: Case):
"""
A case is in lab preparation if at least one sample is not prepared yet.
A case is in sequencing if all samples have been prepared and at least one sample is in sequencing.
"""
samples_in_sequencing = False
for sample in case.samples:
if sample.prepared_at is None:
return CaseStatus.LAB_PREPARATION
if sample.last_sequenced_at is None:
samples_in_sequencing = True
return CaseStatus.SEQUENCING if samples_in_sequencing else CaseStatus.OTHER
Loading
Loading