BTHLABS-61: Service layer refactoring

A journey to fix `ValidationError` in Pocket imports turned service
layer refactoring :D
This commit is contained in:
2025-10-12 18:37:32 +00:00
parent ac7a8dd90e
commit 8b86145519
45 changed files with 1023 additions and 337 deletions

View File

@@ -7,6 +7,7 @@ from bthlabs_jsonrpc_core import register_method
from django import db
from django.http import HttpRequest
from hotpocket_backend.apps.core.rpc import wrap_soa_errors
from hotpocket_soa.services import (
AccessTokensService,
AccountsService,
@@ -17,6 +18,7 @@ LOGGER = logging.getLogger(__name__)
@register_method('accounts.access_tokens.create', namespace='accounts')
@wrap_soa_errors
def create(request: HttpRequest,
auth_key: str,
meta: dict,
@@ -27,7 +29,7 @@ def create(request: HttpRequest,
account_uuid=None,
key=auth_key,
)
except AuthKeysService.AuthKeyNotFound as exception:
except AuthKeysService.NotFound as exception:
LOGGER.error(
'Unable to issue access token: %s',
exception,
@@ -37,7 +39,7 @@ def create(request: HttpRequest,
try:
account = AccountsService().get(pk=auth_key_object.account_uuid)
except AccountsService.AccountNotFound as exception:
except AccountsService.NotFound as exception:
LOGGER.error(
'Unable to issue access token: %s',
exception,

View File

@@ -44,7 +44,7 @@ def check_access_token(request: HttpRequest,
access_token=access_token_object,
update=meta_update,
)
except AccessTokensService.AccessTokenNotFound as exception:
except AccessTokensService.NotFound as exception:
LOGGER.error(
'Access Token not found: account_uuid=`%s` key=`%s`',
request.user.pk,
@@ -52,7 +52,7 @@ def check_access_token(request: HttpRequest,
exc_info=exception,
)
result = False
except AccessTokensService.AccessTokenAccessDenied as exception:
except AccessTokensService.AccessDenied as exception:
LOGGER.error(
'Access Token access denied: account_uuid=`%s` key=`%s`',
request.user.pk,

View File

@@ -4,11 +4,13 @@ from __future__ import annotations
from bthlabs_jsonrpc_core import register_method
from django.http import HttpRequest
from hotpocket_backend.apps.core.rpc import wrap_soa_errors
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
from hotpocket_soa.dto.associations import AssociationOut
@register_method(method='saves.create')
@wrap_soa_errors
def create(request: HttpRequest, url: str) -> AssociationOut:
association = CreateSaveWorkflow().run_rpc(
request=request,

View File

@@ -27,7 +27,7 @@ class UIAccessTokensService:
account_uuid=account_uuid,
pk=pk,
)
except AccessTokensService.AccessTokenNotFound as exception:
except AccessTokensService.NotFound as exception:
LOGGER.error(
'Access Token not found: account_uuid=`%s` pk=`%s`',
account_uuid,
@@ -35,7 +35,7 @@ class UIAccessTokensService:
exc_info=exception,
)
raise Http404()
except AccessTokensService.AccessTokenAccessDenied as exception:
except AccessTokensService.AccessDenied as exception:
LOGGER.error(
'Access Token access denied: account_uuid=`%s` pk=`%s`',
account_uuid,

View File

@@ -34,7 +34,7 @@ class UIAssociationsService:
with_target=True,
allow_archived=allow_archived,
)
except AssociationsService.AssociationNotFound as exception:
except AssociationsService.NotFound as exception:
LOGGER.error(
'Association not found: account_uuid=`%s` pk=`%s`',
account_uuid,
@@ -42,7 +42,7 @@ class UIAssociationsService:
exc_info=exception,
)
raise Http404()
except AssociationsService.AssociationAccessDenied as exception:
except AssociationsService.AccessDenied as exception:
LOGGER.error(
'Association access denied: account_uuid=`%s` pk=`%s`',
account_uuid,

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import csv
import datetime
import logging
import os
import uuid
@@ -13,16 +14,28 @@ from django.utils.timezone import get_current_timezone
from hotpocket_backend.apps.ui.services.workflows import ImportSaveWorkflow
from hotpocket_backend.apps.ui.tasks import import_from_pocket
from hotpocket_common.uuid import uuid7_from_timestamp
from hotpocket_soa.services import SavesService
LOGGER = logging.getLogger(__name__)
class UIImportsService:
def import_from_pocket(self,
*,
job: str,
account_uuid: uuid.UUID,
csv_path: str,
) -> list[tuple[uuid.UUID, uuid.UUID]]:
result = []
LOGGER.info(
'Starting import job: job=`%s` account_uuid=`%s`',
job,
account_uuid,
extra={
'job': job,
},
)
result = []
with db.transaction.atomic():
try:
with open(csv_path, 'r', encoding='utf-8') as csv_file:
@@ -34,22 +47,35 @@ class UIImportsService:
current_timezone = get_current_timezone()
is_header = False
for row in csv_reader:
for row_number, row in enumerate(csv_reader, start=1):
if is_header is False:
is_header = True
continue
timestamp = int(row['time_added'])
save, association = ImportSaveWorkflow().run(
account_uuid=account_uuid,
url=row['url'],
title=row['title'],
pk=uuid7_from_timestamp(timestamp),
created_at=datetime.datetime.fromtimestamp(
timestamp, tz=current_timezone,
),
)
try:
save, association = ImportSaveWorkflow().run(
account_uuid=account_uuid,
url=row['url'],
title=row['title'],
pk=uuid7_from_timestamp(timestamp),
created_at=datetime.datetime.fromtimestamp(
timestamp, tz=current_timezone,
),
)
except SavesService.Invalid as exception:
LOGGER.error(
'Import error: row_number=`%d` url=`%s` exception=`%s`',
row_number,
row['url'],
exception,
exc_info=exception,
extra={
'job': job,
},
)
continue
result.append((save.pk, association.pk))
finally:
@@ -64,6 +90,7 @@ class UIImportsService:
) -> AsyncResult:
return import_from_pocket.apply_async(
kwargs={
'job': str(uuid.uuid4()),
'account_uuid': account_uuid,
'csv_path': csv_path,
},

View File

@@ -19,7 +19,7 @@ class UISavesService:
def get_or_404(self, *, pk: uuid.UUID) -> SaveOut:
try:
return SavesService().get(pk=pk)
except SavesService.SaveNotFound as exception:
except SavesService.NotFound as exception:
LOGGER.error(
'Save not found: pk=`%s`', pk, exc_info=exception,
)

View File

@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from bthlabs_jsonrpc_core import JSONRPCInternalError
from django.contrib import messages
from django.core.exceptions import ValidationError
import django.db
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
@@ -14,7 +12,6 @@ from hotpocket_backend.apps.accounts.types import PAccount
from hotpocket_soa.dto.associations import AssociationOut
from hotpocket_soa.dto.celery import AsyncResultOut
from hotpocket_soa.dto.saves import SaveIn, SaveOut
from hotpocket_soa.services import SavesService
from .base import SaveWorkflow
@@ -73,14 +70,8 @@ class CreateSaveWorkflow(SaveWorkflow):
account: PAccount,
url: str,
) -> AssociationOut:
try:
save, association, processing_result = self.create_associate_and_process(
account, url,
)
save, association, processing_result = self.create_associate_and_process(
account, url,
)
return association
except SavesService.SavesServiceError as exception:
if isinstance(exception.__cause__, ValidationError) is True:
raise JSONRPCInternalError(data=exception.__cause__)
raise
return association

View File

@@ -11,6 +11,7 @@ LOGGER = logging.getLogger(__name__)
@shared_task
def import_from_pocket(*,
job: str,
account_uuid: uuid.UUID,
csv_path: str,
) -> list[tuple[uuid.UUID, uuid.UUID]]:
@@ -18,6 +19,7 @@ def import_from_pocket(*,
try:
return UIImportsService().import_from_pocket(
job=job,
account_uuid=account_uuid,
csv_path=csv_path,
)