You've already forked hotpocket
BTHLABS-61: Service layer refactoring
A journey to fix `ValidationError` in Pocket imports turned service layer refactoring :D
This commit is contained in:
@@ -6,6 +6,7 @@ import hmac
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
import uuid6
|
||||
|
||||
@@ -15,6 +16,10 @@ from hotpocket_soa.dto.accounts import (
|
||||
AccessTokenMetaUpdateIn,
|
||||
AccessTokensQuery,
|
||||
)
|
||||
from hotpocket_soa.exceptions.backend import (
|
||||
Invalid as InvalidError,
|
||||
NotFound as NotFoundError,
|
||||
)
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,7 +28,10 @@ class AccessTokensService:
|
||||
class AccessTokensServiceError(Exception):
|
||||
pass
|
||||
|
||||
class AccessTokenNotFound(AccessTokensServiceError):
|
||||
class Invalid(InvalidError, AccessTokensServiceError):
|
||||
pass
|
||||
|
||||
class NotFound(NotFoundError, AccessTokensServiceError):
|
||||
pass
|
||||
|
||||
def create(self,
|
||||
@@ -32,20 +40,23 @@ class AccessTokensService:
|
||||
origin: str,
|
||||
meta: dict,
|
||||
) -> AccessToken:
|
||||
pk = uuid6.uuid7()
|
||||
key = hmac.new(
|
||||
settings.SECRET_KEY.encode('ascii'),
|
||||
msg=pk.bytes,
|
||||
digestmod=hashlib.sha256,
|
||||
)
|
||||
try:
|
||||
pk = uuid6.uuid7()
|
||||
key = hmac.new(
|
||||
settings.SECRET_KEY.encode('ascii'),
|
||||
msg=pk.bytes,
|
||||
digestmod=hashlib.sha256,
|
||||
)
|
||||
|
||||
return AccessToken.objects.create(
|
||||
pk=pk,
|
||||
account_uuid=account_uuid,
|
||||
key=key.hexdigest(),
|
||||
origin=origin,
|
||||
meta=meta,
|
||||
)
|
||||
return AccessToken.objects.create(
|
||||
pk=pk,
|
||||
account_uuid=account_uuid,
|
||||
key=key.hexdigest(),
|
||||
origin=origin,
|
||||
meta=meta,
|
||||
)
|
||||
except ValidationError as exception:
|
||||
raise self.Invalid.from_django_validation_error(exception)
|
||||
|
||||
def get(self, *, pk: uuid.UUID) -> AccessToken:
|
||||
try:
|
||||
@@ -53,7 +64,7 @@ class AccessTokensService:
|
||||
|
||||
return query_set.get(pk=pk)
|
||||
except AccessToken.DoesNotExist as exception:
|
||||
raise self.AccessTokenNotFound(
|
||||
raise self.NotFound(
|
||||
f'Access Token not found: pk=`{pk}`',
|
||||
) from exception
|
||||
|
||||
@@ -63,7 +74,7 @@ class AccessTokensService:
|
||||
|
||||
return query_set.get(key=key)
|
||||
except AccessToken.DoesNotExist as exception:
|
||||
raise self.AccessTokenNotFound(
|
||||
raise self.NotFound(
|
||||
f'Access Token not found: key=`{key}`',
|
||||
) from exception
|
||||
|
||||
@@ -98,7 +109,7 @@ class AccessTokensService:
|
||||
pk: uuid.UUID,
|
||||
update: AccessTokenMetaUpdateIn,
|
||||
) -> AccessToken:
|
||||
access_token = AccessToken.active_objects.get(pk=pk)
|
||||
access_token = self.get(pk=pk)
|
||||
|
||||
next_meta = {
|
||||
**(access_token.meta or {}),
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import uuid
|
||||
|
||||
from hotpocket_backend.apps.accounts.models import Account
|
||||
from hotpocket_soa.exceptions.backend import NotFound as NotFoundError
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -13,7 +14,7 @@ class AccountsService:
|
||||
class AccountsServiceError(Exception):
|
||||
pass
|
||||
|
||||
class AccountNotFound(AccountsServiceError):
|
||||
class NotFound(NotFoundError, AccountsServiceError):
|
||||
pass
|
||||
|
||||
def get(self, *, pk: uuid.UUID) -> Account:
|
||||
@@ -22,6 +23,6 @@ class AccountsService:
|
||||
|
||||
return query_set.get(pk=pk)
|
||||
except Account.DoesNotExist as exception:
|
||||
raise self.AccountNotFound(
|
||||
raise self.NotFound(
|
||||
f'Account not found: pk=`{pk}`',
|
||||
) from exception
|
||||
|
||||
@@ -5,11 +5,17 @@ import datetime
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.timezone import now
|
||||
import uuid6
|
||||
|
||||
from hotpocket_backend.apps.accounts.models import AuthKey
|
||||
from hotpocket_backend.apps.core.conf import settings
|
||||
from hotpocket_soa.exceptions.backend import (
|
||||
InternalError,
|
||||
Invalid as InvalidError,
|
||||
NotFound as NotFoundError,
|
||||
)
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,22 +24,25 @@ class AuthKeysService:
|
||||
class AuthKeysServiceError(Exception):
|
||||
pass
|
||||
|
||||
class AuthKeyNotFound(AuthKeysServiceError):
|
||||
class Invalid(InvalidError, AuthKeysServiceError):
|
||||
pass
|
||||
|
||||
class AuthKeyExpired(AuthKeysServiceError):
|
||||
class NotFound(NotFoundError, AuthKeysServiceError):
|
||||
pass
|
||||
|
||||
class AuthKeyAccessDenied(AuthKeysServiceError):
|
||||
class Expired(InternalError, AuthKeysServiceError):
|
||||
pass
|
||||
|
||||
def create(self, *, account_uuid: uuid.UUID) -> AuthKey:
|
||||
key = str(uuid6.uuid7())
|
||||
try:
|
||||
key = str(uuid6.uuid7())
|
||||
|
||||
return AuthKey.objects.create(
|
||||
account_uuid=account_uuid,
|
||||
key=key,
|
||||
)
|
||||
return AuthKey.objects.create(
|
||||
account_uuid=account_uuid,
|
||||
key=key,
|
||||
)
|
||||
except ValidationError as exception:
|
||||
raise self.Invalid.from_django_validation_error(exception)
|
||||
|
||||
def get(self, *, pk: uuid.UUID) -> AuthKey:
|
||||
try:
|
||||
@@ -41,7 +50,7 @@ class AuthKeysService:
|
||||
|
||||
return query_set.get(pk=pk)
|
||||
except AuthKey.DoesNotExist as exception:
|
||||
raise self.AuthKeyNotFound(
|
||||
raise self.NotFound(
|
||||
f'Auth Key not found: pk=`{pk}`',
|
||||
) from exception
|
||||
|
||||
@@ -56,17 +65,17 @@ class AuthKeysService:
|
||||
|
||||
if ttl > 0:
|
||||
if result.created_at < now() - datetime.timedelta(seconds=ttl):
|
||||
raise self.AuthKeyExpired(
|
||||
raise self.Expired(
|
||||
f'Auth Key expired: pk=`{key}`',
|
||||
)
|
||||
|
||||
if result.consumed_at is not None:
|
||||
raise self.AuthKeyExpired(
|
||||
raise self.Expired(
|
||||
f'Auth Key already consumed: pk=`{key}`',
|
||||
)
|
||||
|
||||
return result
|
||||
except AuthKey.DoesNotExist as exception:
|
||||
raise self.AuthKeyNotFound(
|
||||
raise self.NotFound(
|
||||
f'Auth Key not found: key=`{key}`',
|
||||
) from exception
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import typing
|
||||
|
||||
from bthlabs_jsonrpc_core.exceptions import BaseJSONRPCError
|
||||
from bthlabs_jsonrpc_django import (
|
||||
DjangoExecutor,
|
||||
DjangoJSONRPCSerializer,
|
||||
JSONRPCView as BaseJSONRPCView,
|
||||
)
|
||||
from django.core.exceptions import ValidationError
|
||||
import uuid6
|
||||
|
||||
from hotpocket_soa.exceptions.frontend import SOAError
|
||||
|
||||
|
||||
class SOAJSONRPCError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32000
|
||||
ERROR_MESSAGE = 'SOA Error'
|
||||
|
||||
def to_rpc(self) -> dict:
|
||||
exception = typing.cast(SOAError, self.data)
|
||||
|
||||
code = (
|
||||
exception.code
|
||||
if exception.code is not None
|
||||
else self.ERROR_CODE
|
||||
)
|
||||
|
||||
return {
|
||||
'code': code,
|
||||
'message': exception.message or self.ERROR_MESSAGE,
|
||||
'data': exception.data,
|
||||
}
|
||||
|
||||
|
||||
class JSONRPCSerializer(DjangoJSONRPCSerializer):
|
||||
STRING_COERCIBLE_TYPES: typing.Any = (
|
||||
@@ -18,30 +41,6 @@ class JSONRPCSerializer(DjangoJSONRPCSerializer):
|
||||
uuid6.UUID,
|
||||
)
|
||||
|
||||
def serialize_value(self, value: typing.Any) -> typing.Any:
|
||||
if isinstance(value, ValidationError):
|
||||
result: typing.Any = None
|
||||
|
||||
if hasattr(value, 'error_dict') is True:
|
||||
result = {}
|
||||
for field, errors in value.error_dict.items():
|
||||
result[field] = [
|
||||
error.code
|
||||
for error
|
||||
in errors
|
||||
]
|
||||
elif hasattr(value, 'error_list') is True:
|
||||
result = [
|
||||
error.code
|
||||
for error in value.error_list
|
||||
]
|
||||
else:
|
||||
result = value.code
|
||||
|
||||
return self.serialize_value(result)
|
||||
|
||||
return super().serialize_value(value)
|
||||
|
||||
|
||||
class Executor(DjangoExecutor):
|
||||
serializer = JSONRPCSerializer
|
||||
@@ -49,3 +48,14 @@ class Executor(DjangoExecutor):
|
||||
|
||||
class JSONRPCView(BaseJSONRPCView):
|
||||
executor = Executor
|
||||
|
||||
|
||||
def wrap_soa_errors(func: typing.Callable) -> typing.Callable:
|
||||
@functools.wraps(func)
|
||||
def decorator(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except SOAError as exception:
|
||||
raise SOAJSONRPCError(exception)
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -5,6 +5,7 @@ import datetime
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
|
||||
@@ -15,6 +16,10 @@ from hotpocket_soa.dto.associations import (
|
||||
AssociationsQuery,
|
||||
AssociationUpdateIn,
|
||||
)
|
||||
from hotpocket_soa.exceptions.backend import (
|
||||
Invalid as InvalidError,
|
||||
NotFound as NotFoundError,
|
||||
)
|
||||
|
||||
from .saves import SavesService
|
||||
|
||||
@@ -25,7 +30,10 @@ class AssociationsService:
|
||||
class AssociationsServiceError(Exception):
|
||||
pass
|
||||
|
||||
class AssociationNotFound(AssociationsServiceError):
|
||||
class Invalid(InvalidError, AssociationsServiceError):
|
||||
pass
|
||||
|
||||
class NotFound(NotFoundError, AssociationsServiceError):
|
||||
pass
|
||||
|
||||
@property
|
||||
@@ -46,30 +54,33 @@ class AssociationsService:
|
||||
pk: uuid.UUID | None = None,
|
||||
created_at: datetime.datetime | None = None,
|
||||
) -> Association:
|
||||
save = SavesService().get(pk=save_uuid)
|
||||
try:
|
||||
save = SavesService().get(pk=save_uuid)
|
||||
|
||||
defaults = dict(
|
||||
account_uuid=account_uuid,
|
||||
target=save,
|
||||
)
|
||||
defaults = dict(
|
||||
account_uuid=account_uuid,
|
||||
target=save,
|
||||
)
|
||||
|
||||
if pk is not None:
|
||||
defaults['id'] = pk
|
||||
if pk is not None:
|
||||
defaults['id'] = pk
|
||||
|
||||
result, created = Association.objects.get_or_create(
|
||||
account_uuid=account_uuid,
|
||||
deleted_at__isnull=True,
|
||||
target=save,
|
||||
archived_at__isnull=True,
|
||||
defaults=defaults,
|
||||
)
|
||||
result, created = Association.objects.get_or_create(
|
||||
account_uuid=account_uuid,
|
||||
deleted_at__isnull=True,
|
||||
target=save,
|
||||
archived_at__isnull=True,
|
||||
defaults=defaults,
|
||||
)
|
||||
|
||||
if created is True:
|
||||
if created_at is not None:
|
||||
result.created_at = created_at
|
||||
result.save()
|
||||
if created is True:
|
||||
if created_at is not None:
|
||||
result.created_at = created_at
|
||||
result.save()
|
||||
|
||||
return result
|
||||
return result
|
||||
except ValidationError as exception:
|
||||
raise self.Invalid.from_django_validation_error(exception)
|
||||
|
||||
def get(self,
|
||||
*,
|
||||
@@ -87,7 +98,7 @@ class AssociationsService:
|
||||
|
||||
return query_set.get(pk=pk)
|
||||
except Association.DoesNotExist as exception:
|
||||
raise self.AssociationNotFound(
|
||||
raise self.NotFound(
|
||||
f'Association not found: pk=`{pk}`',
|
||||
) from exception
|
||||
|
||||
@@ -112,21 +123,24 @@ class AssociationsService:
|
||||
pk: uuid.UUID,
|
||||
update: AssociationUpdateIn,
|
||||
) -> Association:
|
||||
association = self.get(pk=pk)
|
||||
association.target_title = update.target_title
|
||||
association.target_description = update.target_description
|
||||
try:
|
||||
association = self.get(pk=pk)
|
||||
association.target_title = update.target_title
|
||||
association.target_description = update.target_description
|
||||
|
||||
next_target_meta = {
|
||||
**(association.target_meta or {}),
|
||||
}
|
||||
next_target_meta = {
|
||||
**(association.target_meta or {}),
|
||||
}
|
||||
|
||||
next_target_meta.pop('title', None)
|
||||
next_target_meta.pop('description', None)
|
||||
association.target_meta = next_target_meta
|
||||
next_target_meta.pop('title', None)
|
||||
next_target_meta.pop('description', None)
|
||||
association.target_meta = next_target_meta
|
||||
|
||||
association.save()
|
||||
association.save()
|
||||
|
||||
return association
|
||||
return association
|
||||
except ValidationError as exception:
|
||||
raise self.Invalid.from_django_validation_error(exception)
|
||||
|
||||
def archive(self, *, pk: uuid.UUID) -> bool:
|
||||
association = self.get(pk=pk)
|
||||
|
||||
@@ -5,19 +5,27 @@ import hashlib
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
|
||||
from hotpocket_backend.apps.core.services import get_adapter
|
||||
from hotpocket_backend.apps.saves.models import Save
|
||||
from hotpocket_backend.apps.saves.types import PSaveAdapter
|
||||
from hotpocket_soa.dto.saves import ImportedSaveIn, SaveIn, SavesQuery
|
||||
from hotpocket_soa.exceptions.backend import (
|
||||
Invalid as InvalidError,
|
||||
NotFound as NotFoundError,
|
||||
)
|
||||
|
||||
|
||||
class SavesService:
|
||||
class SavesServiceError(Exception):
|
||||
pass
|
||||
|
||||
class SaveNotFound(SavesServiceError):
|
||||
class Invalid(InvalidError, SavesServiceError):
|
||||
pass
|
||||
|
||||
class NotFound(NotFoundError, SavesServiceError):
|
||||
pass
|
||||
|
||||
@property
|
||||
@@ -36,35 +44,38 @@ class SavesService:
|
||||
account_uuid: uuid.UUID,
|
||||
save: SaveIn | ImportedSaveIn,
|
||||
) -> Save:
|
||||
key = hashlib.sha256(save.url.encode('utf-8')).hexdigest()
|
||||
try:
|
||||
key = hashlib.sha256(save.url.encode('utf-8')).hexdigest()
|
||||
|
||||
defaults = dict(
|
||||
account_uuid=account_uuid,
|
||||
key=key,
|
||||
url=save.url,
|
||||
)
|
||||
defaults = dict(
|
||||
account_uuid=account_uuid,
|
||||
key=key,
|
||||
url=save.url,
|
||||
)
|
||||
|
||||
save_object, created = Save.objects.get_or_create(
|
||||
key=key,
|
||||
deleted_at__isnull=True,
|
||||
defaults=defaults,
|
||||
)
|
||||
save_object, created = Save.objects.get_or_create(
|
||||
key=key,
|
||||
deleted_at__isnull=True,
|
||||
defaults=defaults,
|
||||
)
|
||||
|
||||
if created is True:
|
||||
save_object.is_netloc_banned = save.is_netloc_banned
|
||||
if created is True:
|
||||
save_object.is_netloc_banned = save.is_netloc_banned
|
||||
|
||||
if isinstance(save, ImportedSaveIn) is True:
|
||||
save_object.title = save.title # type: ignore[union-attr]
|
||||
if isinstance(save, ImportedSaveIn) is True:
|
||||
save_object.title = save.title # type: ignore[union-attr]
|
||||
|
||||
save_object.save()
|
||||
save_object.save()
|
||||
|
||||
return save_object
|
||||
return save_object
|
||||
except ValidationError as exception:
|
||||
raise self.Invalid.from_django_validation_error(exception)
|
||||
|
||||
def get(self, *, pk: uuid.UUID) -> Save:
|
||||
try:
|
||||
return Save.active_objects.get(pk=pk)
|
||||
except Save.DoesNotExist as exception:
|
||||
raise self.SaveNotFound(
|
||||
raise self.NotFound(
|
||||
f'Save not found: pk=`{pk}`',
|
||||
) from exception
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user