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 17 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, Summary
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 @@ -168,3 +169,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[str]) -> list[Summary]:
orders_param = "order_ids[]=".join([f"{order_id}," for order_id in order_ids]).strip(",")
endpoint = f"summaries?{orders_param}"
seallard marked this conversation as resolved.
Show resolved Hide resolved
response = self.query_trailblazer(command=endpoint, request_body={}, method=APIMethods.GET)
response_data = SummariesResponse.model_validate(response)
return response_data.summaries
14 changes: 14 additions & 0 deletions cg/apps/tb/dto/summary_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pydantic import BaseModel


class Summary(BaseModel):
order_id: int
total: int
seallard marked this conversation as resolved.
Show resolved Hide resolved
delivered: int
running: int
cancelled: int
failed: int


class SummariesResponse(BaseModel):
summaries: list[Summary]
4 changes: 0 additions & 4 deletions cg/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,3 @@ class ArchiveJobFailedError(CgError):

class XMLError(CgError):
"""Exception raised when something is wrong with the content of an XML file."""


class OrderNotFoundError(CgError):
"""Exception raised when an order is not found."""
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.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_data=order_in, cases=result["records"])

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 | None = Field(default=False, alias="includeSummary")
9 changes: 9 additions & 0 deletions cg/server/dto/orders/orders_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
from cg.constants import Workflow


class OrderSummary(BaseModel):
total: int
delivered: int | None = None
running: int | None = None
cancelled: int | None = None
failed: int | None = None


class Order(BaseModel):
customer_id: str
ticket_id: int
order_date: str
order_id: int
workflow: Workflow
summary: OrderSummary | None = None


class OrdersResponse(BaseModel):
Expand Down
23 changes: 23 additions & 0 deletions cg/server/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from cg.apps.osticket import OsTicket
from cg.store.database import initialize_database
from cg.store.store import Store
from cg.apps.tb.api import TrailblazerAPI
from cg.services.order_service.order_service import OrderService


class FlaskLims(LimsAPI):
Expand Down Expand Up @@ -45,10 +47,31 @@ 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()
order_service = OrderService(store=db, analysis_client=analysis_client)
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)
File renamed without changes.
6 changes: 6 additions & 0 deletions cg/services/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,20 @@
from cg.exc import OrderNotFoundError
from cg.apps.tb.api import TrailblazerAPI
from cg.apps.tb.dto.summary_response import Summary
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.order_service.exceptions import OrderNotFoundError
from cg.services.order_service.utils import create_order_response, create_orders_response
from cg.store.models import Order
from cg.store.models import Case, Order
from cg.store.store import Store


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

def get_order(self, order_id: int) -> OrderResponse:
order: Order | None = self.store.get_order_by_id(order_id)
Expand All @@ -19,14 +23,13 @@ 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)

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(
orders: list[Order] = self.store.get_orders(
workflow=orders_request.workflow, limit=orders_request.limit
)
summaries: list[Summary] = []
if orders_request.include_summary:
summaries = self.analysis_client.get_summaries(orders)
return create_orders_response(database_orders=orders, summaries=summaries)

def create_order(self, order_data: OrderIn, cases: list[Case]) -> OrderResponse:
"""Creates an order and links it to the given cases."""
Expand Down
41 changes: 41 additions & 0 deletions cg/services/order_service/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from cg.apps.tb.dto.summary_response import Summary
from cg.server.dto.orders.orders_response import Order, OrderSummary, OrdersResponse
from cg.store.models import Order as DatabaseOrder


def parse_order(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(
database_orders: list[DatabaseOrder], summaries: list[Summary]
) -> OrdersResponse:
orders = [parse_order(order) for order in database_orders]
if summaries:
_add_summaries_to_orders(orders=orders, summaries=summaries)
return OrdersResponse(orders=orders)


def _add_summaries_to_orders(orders: list[Order], summaries: list[Summary]) -> list[Order]:
summary_mapping: dict = {summary.order_id: summary for summary in summaries}
for order in orders:
summary: Summary = summary_mapping.get(order.order_id)
if summary:
order.summary = OrderSummary(
total=summary.total,
delivered=summary.delivered,
running=summary.running,
cancelled=summary.cancelled,
failed=summary.failed,
)
seallard marked this conversation as resolved.
Show resolved Hide resolved
return orders


def create_order_response(order: DatabaseOrder) -> Order:
return parse_order(order)
21 changes: 0 additions & 21 deletions cg/services/orders/utils.py

This file was deleted.

26 changes: 14 additions & 12 deletions cg/store/crud/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from cg.constants import FlowCellStatus, Workflow
from cg.constants.constants import CaseActions, CustomerId, PrepCategory, SampleType
from cg.exc import CaseNotFoundError, CgError
from cg.server.dto.orders.orders_request import OrdersRequest
from cg.store.base import BaseHandler
from cg.store.filters.status_analysis_filters import (
AnalysisFilter,
Expand Down Expand Up @@ -1729,23 +1730,24 @@ def get_all_pools_to_deliver(self) -> list[Pool]:
)
return records.all()

def get_orders_by_workflow(
self, workflow: str | None = None, limit: int | None = None
) -> list[Order]:
"""Returns a list of entries in Order. The output is filtered on workflow and limited, if given."""
orders: Query = self._get_query(table=Order)
order_filter_functions: list[Callable] = [OrderFilter.FILTER_ORDERS_BY_WORKFLOW]
orders: Query = apply_order_filters(
orders=orders, filter_functions=order_filter_functions, workflow=workflow
)
return orders.limit(limit).all()
def get_orders(self, workflow: Workflow | None = None, limit: int | None = None) -> list[Order]:
seallard marked this conversation as resolved.
Show resolved Hide resolved
filters: list[OrderFilter] = [
OrderFilter.BY_WORKFLOW,
OrderFilter.APPLY_LIMIT,
]
return apply_order_filters(
orders=self._get_query(Order),
filters=filters,
workflow=workflow,
limit=limit,
).all()

def get_order_by_id(self, order_id: int) -> Order | None:
"""Returns the entry in Order matching the given id."""
orders: Query = self._get_query(table=Order)
order_filter_functions: list[Callable] = [OrderFilter.FILTER_ORDERS_BY_ID]
order_filter_functions: list[Callable] = [OrderFilter.BY_ID]
orders: Query = apply_order_filters(
orders=orders, filter_functions=order_filter_functions, id=order_id
orders=orders, filters=order_filter_functions, id=order_id
)
return orders.first()

Expand Down
Loading
Loading