Skip to content

Commit

Permalink
Merge branch 'master' into patch_taxprofiler_versions
Browse files Browse the repository at this point in the history
  • Loading branch information
sofstam authored Sep 3, 2024
2 parents 9a2eff5 + f19de7d commit 845c372
Show file tree
Hide file tree
Showing 37 changed files with 596 additions and 518 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 62.2.1
current_version = 62.2.5
commit = True
tag = True
tag_name = v{new_version}
Expand Down
2 changes: 1 addition & 1 deletion cg/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__title__ = "cg"
__version__ = "62.2.1"
__version__ = "62.2.5"
17 changes: 13 additions & 4 deletions cg/apps/invoice/render.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import datetime as dt
from pathlib import Path

from openpyxl import Workbook, load_workbook
from openpyxl.styles import Border, Font, PatternFill, Side
from pkg_resources import resource_filename

from cg.constants import FileExtensions
from cg.utils.files import get_project_root_dir


def render_xlsx(data: dict) -> Workbook:
Expand Down Expand Up @@ -34,11 +37,17 @@ def render_xlsx(data: dict) -> Workbook:
}]
}
"""
pkg_dir = __name__.rpartition(".")[0]
project_root_dir = get_project_root_dir()
sample_type = "pool" if data["pooled_samples"] else "sample"
costcenter = data["cost_center"]
template_path = resource_filename(pkg_dir, f"templates/{costcenter}_{sample_type}_invoice.xlsx")
workbook = load_workbook(template_path)
template_path = Path(
project_root_dir,
"apps",
"invoice",
"templates",
f"{costcenter}_{sample_type}_invoice{FileExtensions.XLSX}",
)
workbook = load_workbook(template_path.as_posix())
if data["pooled_samples"]:
worksheet = workbook["Bilaga Prover"]
worksheet["C1"] = costcenter.upper()
Expand Down
1 change: 0 additions & 1 deletion cg/cli/post_process/post_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,4 @@ def post_process_sequencing_run(context: CGConfig, run_name: str, dry_run: bool)
post_processing_service.post_process(run_name=run_name, dry_run=dry_run)


post_process_group: click.Group
post_process_group.add_command(post_process_sequencing_run)
59 changes: 27 additions & 32 deletions cg/clients/freshdesk/freshdesk_client.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import logging
from http import HTTPStatus
from pathlib import Path

from pydantic import ValidationError
from requests import RequestException, Response, Session
from requests import Session
from requests.adapters import HTTPAdapter
from urllib3 import Retry

from cg.clients.freshdesk.constants import EndPoints
from cg.clients.freshdesk.exceptions import (
FreshdeskAPIException,
FreshdeskModelException,
)
from cg.clients.freshdesk.models import TicketCreate, TicketResponse

LOG = logging.getLogger(__name__)
from cg.clients.freshdesk.models import ReplyCreate, TicketCreate, TicketResponse
from cg.clients.freshdesk.utils import handle_client_errors, prepare_attachments


class FreshdeskClient:
Expand All @@ -24,33 +18,23 @@ def __init__(self, base_url: str, api_key: str):
self.api_key = api_key
self.session = self._get_session()

def create_ticket(self, ticket: TicketCreate) -> TicketResponse:
"""Create a ticket."""
LOG.debug(ticket.model_dump_json())
try:
response: Response = self.session.post(
url=self._url(EndPoints.TICKETS),
json=ticket.model_dump(exclude_none=True),
)
response.raise_for_status()
return TicketResponse.model_validate(response.json())
except RequestException as error:
LOG.error(f"Could not create ticket: {error}")
raise FreshdeskAPIException(error) from error
except ValidationError as error:
LOG.error(f"Response from Freshdesk does not fit model: {TicketResponse}.\n{error}")
raise FreshdeskModelException(error) from error
@handle_client_errors
def create_ticket(self, ticket: TicketCreate, attachments: list[Path] = None) -> TicketResponse:
"""Create a ticket with multipart form data."""
multipart_data = ticket.to_multipart_data()
files = prepare_attachments(attachments) if attachments else None

def _url(self, endpoint: str) -> str:
"""Get the full URL for the endpoint."""
return f"{self.base_url}{endpoint}"
response = self.session.post(
url=f"{self.base_url}{EndPoints.TICKETS}", data=multipart_data, files=files
)
response.raise_for_status()
return TicketResponse.model_validate(response.json())

@handle_client_errors
def _get_session(self) -> Session:
"""Configures and sets a session to be used for requests."""
session = Session()
self._configure_retries(session)
session.auth = (self.api_key, "X")
session.headers.update({"Content-Type": "application/json"})
self._configure_retries(session)
return session

@staticmethod
Expand All @@ -69,3 +53,14 @@ def _configure_retries(session: Session) -> None:
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)

@handle_client_errors
def reply_to_ticket(self, reply: ReplyCreate, attachments: list[Path] = None) -> None:
"""Send a reply to an existing ticket in Freshdesk."""
url = f"{self.base_url}{EndPoints.TICKETS}/{reply.ticket_number}/reply"

files = prepare_attachments(attachments) if attachments else None
multipart_data = reply.to_multipart_data()

response = self.session.post(url=url, data=multipart_data, files=files)
response.raise_for_status()
53 changes: 39 additions & 14 deletions cg/clients/freshdesk/models.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,61 @@
from datetime import datetime
from typing import Tuple, Union

from pydantic import BaseModel
from pydantic import BaseModel, EmailStr, Field

from cg.clients.freshdesk.constants import Priority, Source, Status


class TicketCreate(BaseModel):
"""Freshdesk ticket."""

attachments: list[dict[str, str]] = []
email: str
attachments: list[str | bytes] = Field(default_factory=list)
email: EmailStr
email_config_id: int | None = None
description: str
name: str
priority: int = Priority.LOW
source: int = Source.EMAIL
status: int = Status.OPEN
subject: str
tags: list[str] = []
type: str | None = None
custom_fields: dict[str, str | int | float | None] = Field(default_factory=dict)

def to_multipart_data(self) -> list[Tuple[str, str | int | bytes]]:
"""Custom converter to multipart form data."""
multipart_data = []

for field, value in self.model_dump(exclude_none=True).items():
if isinstance(value, list):
multipart_data.extend([(f"{field}[]", v) for v in value])
elif isinstance(value, dict):
multipart_data.extend([(f"{field}[{k}]", v) for k, v in value.items()])
else:
multipart_data.append((field, value))

return multipart_data


class TicketResponse(BaseModel):
"""Freshdesk ticket response."""
"""Response from Freshdesk"""

attachments: list[dict[str, str]] = []
created_at: datetime | None = None
email: str
id: int
name: str | None = None
priority: int
source: int
status: int
description: str
subject: str
tags: list[str] = []
type: str | None = None
to_emails: list[str] | None = None
status: int
priority: int


class ReplyCreate(BaseModel):
"""Reply to a ticket."""

ticket_number: str
body: str

def to_multipart_data(self) -> list[Tuple[str, Union[str, int, bytes]]]:
"""Custom converter to multipart form data."""
multipart_data = [
("body", self.body),
]
return multipart_data
86 changes: 86 additions & 0 deletions cg/clients/freshdesk/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import logging
from functools import wraps
from pathlib import Path
from tempfile import TemporaryDirectory

from pydantic import ValidationError
from requests import ConnectionError, HTTPError
from requests.exceptions import MissingSchema

from cg.clients.freshdesk.exceptions import (
FreshdeskAPIException,
FreshdeskModelException,
)
from cg.constants.constants import FileFormat
from cg.io.controller import WriteFile

LOG = logging.getLogger(__name__)


def extract_error_detail(error):
"""Extract detailed error information from HTTPError."""
if error.response is not None:
try:
return error.response.json()
except ValueError:
return error.response.text
return None


def log_and_raise(exception_class, message, error):
"""Log the error and raise the appropriate exception."""
LOG.error(message)
raise exception_class(error) from error


def handle_client_errors(func):
"""Decorator to handle and log errors in Freshdesk client methods."""

@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except HTTPError as error:
error_detail = extract_error_detail(error)
log_and_raise(
FreshdeskAPIException,
f"Failed request to Freshdesk: {error} - Status code: "
f"{error.response.status_code if error.response else 'N/A'}, Details: {error_detail}",
error,
)
except (MissingSchema, ConnectionError) as error:
log_and_raise(FreshdeskAPIException, f"Request to Freshdesk failed: {error}", error)
except ValidationError as error:
log_and_raise(
FreshdeskModelException, f"Invalid response from Freshdesk: {error}", error
)
except ValueError as error:
log_and_raise(FreshdeskAPIException, f"Operation failed: {error}", error)
except Exception as error:
log_and_raise(
FreshdeskAPIException, f"Unexpected error in Freshdesk client: {error}", error
)

return wrapper


def prepare_attachments(attachments: list[Path]) -> list[tuple[str, tuple[str, bytes]]]:
"""Prepare the attachments for a request."""
return [
("attachments[]", (attachment.name, open(attachment, "rb"))) for attachment in attachments
]


def create_temp_attachment_file(content: dict, file_name: Path) -> TemporaryDirectory:
"""Create a file-based attachment."""
if content and file_name:
directory = TemporaryDirectory()
WriteFile.write_file_from_content(
content=content,
file_format=FileFormat.JSON,
file_path=Path(directory.name, "order.json"),
)
return directory
else:
LOG.error("Content or file path is None. Cannot create file attachment.")
raise ValueError("Both content and file path must be provided and cannot be None")
1 change: 1 addition & 0 deletions cg/constants/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ class FileExtensions(StrEnum):
TSV: str = ".tsv"
TXT: str = ".txt"
VCF: str = ".vcf"
XLSX: str = ".xlsx"
XML: str = ".xml"
YAML: str = ".yaml"

Expand Down
6 changes: 4 additions & 2 deletions cg/constants/report.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
"""Delivery report constants."""

from importlib.resources import files
from pathlib import Path

from cg.constants import DataDelivery
from cg.constants.constants import CancerAnalysisType, FileExtensions, Workflow
from cg.constants.subject import Sex
from cg.utils.files import get_project_root_dir

project_root_dir: Path = get_project_root_dir()

DELIVERY_REPORT_FILE_NAME: str = f"delivery-report{FileExtensions.HTML}"
SWEDAC_LOGO_PATH = Path(
files("cg"), "meta", "report", "templates", "static", "images", "SWEDAC_logo.png"
project_root_dir, "meta", "report", "templates", "static", "images", "SWEDAC_logo.png"
)

BALSAMIC_REPORT_ACCREDITED_PANELS: list[str] = ["gmsmyeloid"]
Expand Down
8 changes: 3 additions & 5 deletions cg/meta/orders/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import logging

from cg.apps.lims import LimsAPI
from cg.apps.osticket import OsTicket
from cg.meta.orders.ticket_handler import TicketHandler
from cg.models.orders.order import OrderIn, OrderType
from cg.services.orders.submitters.order_submitter_registry import (
Expand All @@ -28,13 +27,13 @@ def __init__(
self,
lims: LimsAPI,
status: Store,
osticket: OsTicket,
ticket_handler: TicketHandler,
submitter_registry: OrderSubmitterRegistry,
):
super().__init__()
self.lims = lims
self.status = status
self.ticket_handler: TicketHandler = TicketHandler(osticket_api=osticket, status_db=status)
self.ticket_handler = ticket_handler
self.submitter_registry = submitter_registry

def submit(self, project: OrderType, order_in: OrderIn, user_name: str, user_mail: str) -> dict:
Expand All @@ -45,7 +44,7 @@ def submit(self, project: OrderType, order_in: OrderIn, user_name: str, user_mai
submit_handler = self.submitter_registry.get_order_submitter(project)
submit_handler.order_validation_service.validate_order(order_in)
# detect manual ticket assignment
ticket_number: str | None = TicketHandler.parse_ticket_number(order_in.name)
ticket_number: str | None = self.ticket_handler.parse_ticket_number(order_in.name)
if not ticket_number:
ticket_number = self.ticket_handler.create_ticket(
order=order_in, user_name=user_name, user_mail=user_mail, project=project
Expand All @@ -54,7 +53,6 @@ def submit(self, project: OrderType, order_in: OrderIn, user_name: str, user_mai
self.ticket_handler.connect_to_ticket(
order=order_in,
user_name=user_name,
user_mail=user_mail,
project=project,
ticket_number=ticket_number,
)
Expand Down
Loading

0 comments on commit 845c372

Please sign in to comment.