BTHLABS-61: Service layer refactoring
A journey to fix `ValidationError` in Pocket imports turned service layer refactoring :D
This commit is contained in:
parent
ac7a8dd90e
commit
8b86145519
|
@ -11,14 +11,64 @@ on:
|
|||
- "public"
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: "Setup"
|
||||
runs-on: "ubuntu-latest"
|
||||
outputs:
|
||||
SHORT_SHA: ${{ steps.get-build-options.outputs.SHORT_SHA }}
|
||||
BUILD_ARCH: ${{ steps.get-build-options.outputs.BUILD_ARCH }}
|
||||
BUILD_PLATFORM: ${{ steps.get-build-options.outputs.BUILD_PLATFORM }}
|
||||
HOTPOCKET_BACKEND_VERSION: ${{ steps.get-backend-version.outputs.HOTPOCKET_BACKEND_VERSION }}
|
||||
HOTPOCKET_BACKEND_BUILD: ${{ steps.get-backend-version.outputs.HOTPOCKET_BACKEND_BUILD }}
|
||||
steps:
|
||||
- name: "Checkout the code"
|
||||
uses: "actions/checkout@v2"
|
||||
- name: "Get build options"
|
||||
id: "get-build-options"
|
||||
run: |
|
||||
set -x
|
||||
SHORT_SHA="${GITHUB_SHA::8}"
|
||||
BUILD_ARCH="amd64"
|
||||
BUILD_PLATFORM="linux/amd64"
|
||||
if [ "${RUNNER_ARCH}" = "ARM64" ];then
|
||||
BUILD_ARCH="arm64"
|
||||
BUILD_PLATFORM="linux/arm64"
|
||||
fi
|
||||
echo "SHORT_SHA=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_ARCH=$BUILD_ARCH" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_PLATFORM=$BUILD_PLATFORM" >> $GITHUB_OUTPUT
|
||||
- name: "Get `backend` version"
|
||||
id: "get-backend-version"
|
||||
run: |
|
||||
set -x
|
||||
if [ "${GITHUB_REF_NAME}" = "development" ]; then
|
||||
VERSION="${GITHUB_SHA::8}"
|
||||
BUILD="${GITHUB_RUN_NUMBER}"
|
||||
else
|
||||
VERSION="v$(grep -Po '(?<=^version\s=\s")[^"]+' services/backend/pyproject.toml)"
|
||||
BUILD="01"
|
||||
fi
|
||||
echo "HOTPOCKET_BACKEND_VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "HOTPOCKET_BACKEND_BUILD=$BUILD" >> $GITHUB_OUTPUT
|
||||
|
||||
run-checks:
|
||||
name: "Checks"
|
||||
runs-on: "ubuntu-latest"
|
||||
needs:
|
||||
- "setup"
|
||||
steps:
|
||||
- name: "Checkout the code"
|
||||
uses: "actions/checkout@v2"
|
||||
- name: "Set up Docker Buildx"
|
||||
id: "setup-docker-buildx"
|
||||
uses: "docker/setup-buildx-action@v3"
|
||||
with:
|
||||
driver: "remote"
|
||||
endpoint: "tcp://builder-01.bthlab:2375"
|
||||
platforms: "linux/amd64"
|
||||
append: |
|
||||
- endpoint: "tcp://builder-mac-01.bthlab:2375"
|
||||
platforms: "linux/arm64"
|
||||
- name: "Build `postgres` image"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
|
@ -27,6 +77,7 @@ jobs:
|
|||
push: false
|
||||
load: true
|
||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/postgres:15.13-local"
|
||||
platforms: "${{ needs.setup.outputs.BUILD_PLATFORM }}"
|
||||
- name: "Build `keycloak` image"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
|
@ -35,6 +86,7 @@ jobs:
|
|||
push: false
|
||||
load: true
|
||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/keycloak:22.0.3-local"
|
||||
platforms: "${{ needs.setup.outputs.BUILD_PLATFORM }}"
|
||||
- name: "Build `rabbitmq` image"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
|
@ -43,6 +95,7 @@ jobs:
|
|||
push: false
|
||||
load: true
|
||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/rabbitmq:3.10.8-local"
|
||||
platforms: "${{ needs.setup.outputs.BUILD_PLATFORM }}"
|
||||
- name: "Build `backend-ci` image"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
|
@ -52,6 +105,7 @@ jobs:
|
|||
push: false
|
||||
load: true
|
||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:ci-local"
|
||||
platforms: "${{ needs.setup.outputs.BUILD_PLATFORM }}"
|
||||
- name: "Build `packages-ci` image"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
|
@ -61,6 +115,7 @@ jobs:
|
|||
push: false
|
||||
load: true
|
||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-local"
|
||||
platforms: "${{ needs.setup.outputs.BUILD_PLATFORM }}"
|
||||
- name: "Build `extension-ci` image"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
|
@ -70,6 +125,7 @@ jobs:
|
|||
push: false
|
||||
load: true
|
||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/extension:ci-local"
|
||||
platforms: "${{ needs.setup.outputs.BUILD_PLATFORM }}"
|
||||
- name: "Build `apple-ci` image"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
|
@ -79,6 +135,7 @@ jobs:
|
|||
push: false
|
||||
load: true
|
||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/apple:ci-local"
|
||||
platforms: "${{ needs.setup.outputs.BUILD_PLATFORM }}"
|
||||
- name: "Run `backend` checks"
|
||||
run: |
|
||||
set -x
|
||||
|
|
|
@ -8,7 +8,7 @@ ARG APP_USER_UID
|
|||
ARG APP_USER_GID
|
||||
ARG IMAGE_ID
|
||||
|
||||
COPY --chown=$APP_USER_UID:$APP_USER_GID apple/ops/bin/*.sh /srv/bin/
|
||||
# COPY --chown=$APP_USER_UID:$APP_USER_GID apple/ops/bin/*.sh /srv/bin/
|
||||
|
||||
VOLUME ["/srv/node_modules", "/srv/venv"]
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -76,6 +76,18 @@ def starred_association_out(starred_association):
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deleted_save_association(association_factory, deleted_save):
|
||||
return association_factory(target=deleted_save)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deleted_save_association_out(deleted_save_association):
|
||||
return AssociationWithTargetOut.model_validate(
|
||||
deleted_save_association, from_attributes=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def other_account_association(association_factory, other_account):
|
||||
return association_factory(account=other_account)
|
||||
|
@ -124,6 +136,18 @@ def other_account_starred_association_out(other_account_starred_association):
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def other_account_deleted_save_association(association_factory, other_account, deleted_save):
|
||||
return association_factory(account=other_account, target=deleted_save)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def other_account_deleted_save_association_out(other_account_deleted_save_association):
|
||||
return AssociationWithTargetOut.model_validate(
|
||||
other_account_deleted_save_association, from_attributes=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def browsable_associations(association,
|
||||
deleted_association,
|
||||
|
|
|
@ -64,11 +64,25 @@ def pocket_import_banned_netloc_save_spec():
|
|||
})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pocket_import_invalid_url_spec():
|
||||
return PocketImportSaveSpec.model_validate({
|
||||
'title': "This isn't right",
|
||||
'url': 'thisisntright',
|
||||
'time_added': datetime.datetime(
|
||||
2021, 1, 18, 8, 0, 0, 0, tzinfo=datetime.UTC,
|
||||
),
|
||||
'tags': '',
|
||||
'status': 'unread',
|
||||
})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pocket_csv_content(pocket_import_created_save_spec,
|
||||
pocket_import_reused_save_spec,
|
||||
pocket_import_other_account_save_spec,
|
||||
pocket_import_banned_netloc_save_spec,
|
||||
pocket_import_invalid_url_spec,
|
||||
):
|
||||
with io.StringIO() as csv_f:
|
||||
field_names = [
|
||||
|
@ -82,6 +96,7 @@ def pocket_csv_content(pocket_import_created_save_spec,
|
|||
pocket_import_reused_save_spec.dict(),
|
||||
pocket_import_other_account_save_spec.dict(),
|
||||
pocket_import_banned_netloc_save_spec.dict(),
|
||||
pocket_import_invalid_url_spec.dict(),
|
||||
])
|
||||
|
||||
csv_f.seek(0)
|
||||
|
|
|
@ -29,6 +29,20 @@ class AssociationsTestingService:
|
|||
if reference is not None:
|
||||
assert association.updated_at > reference.updated_at
|
||||
|
||||
def assert_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
||||
association = Association.objects.get(pk=pk)
|
||||
assert association.deleted_at is not None
|
||||
|
||||
if reference is not None:
|
||||
assert association.updated_at > reference.updated_at
|
||||
|
||||
def assert_not_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
||||
association = Association.objects.get(pk=pk)
|
||||
assert association.deleted_at is None
|
||||
|
||||
if reference is not None:
|
||||
assert association.updated_at == reference.updated_at
|
||||
|
||||
def assert_starred(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
||||
association = Association.objects.get(pk=pk)
|
||||
assert association.starred_at is not None
|
||||
|
|
|
@ -45,10 +45,12 @@ def test_ok(account,
|
|||
other_account_save_out,
|
||||
pocket_import_other_account_save_spec: PocketImportSaveSpec,
|
||||
pocket_import_banned_netloc_save_spec: PocketImportSaveSpec,
|
||||
pocket_import_invalid_url_spec: PocketImportSaveSpec,
|
||||
mock_saves_process_save_task_apply_async: mock.Mock,
|
||||
):
|
||||
# When
|
||||
result = tasks_module.import_from_pocket(
|
||||
job='test',
|
||||
account_uuid=account.pk,
|
||||
csv_path=str(pocket_csv_file_path),
|
||||
)
|
||||
|
|
|
@ -100,6 +100,54 @@ def test_invalid_all_empty(authenticated_client: Client,
|
|||
assert 'canhazconfirm' in result.context['form'].errors
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_archived(authenticated_client: Client,
|
||||
archived_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.archive', args=(archived_association_out.pk,)),
|
||||
data={
|
||||
'canhazconfirm': 'hai',
|
||||
},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_deleted(authenticated_client: Client,
|
||||
deleted_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.archive', args=(deleted_association_out.pk,)),
|
||||
data={
|
||||
'canhazconfirm': 'hai',
|
||||
},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_found(authenticated_client: Client,
|
||||
null_uuid,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.archive', args=(null_uuid,)),
|
||||
data={
|
||||
'canhazconfirm': 'hai',
|
||||
},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_other_account_association(authenticated_client: Client,
|
||||
other_account_association_out,
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.urls import reverse
|
|||
import pytest
|
||||
from pytest_django import asserts
|
||||
|
||||
from hotpocket_backend.apps.saves.models import Association
|
||||
from hotpocket_backend_testing.services.saves import AssociationsTestingService
|
||||
from hotpocket_common.constants import AssociationsSearchMode
|
||||
|
||||
|
||||
|
@ -35,9 +35,10 @@ def test_ok(authenticated_client: Client,
|
|||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
association_object = Association.objects.get(pk=association_out.pk)
|
||||
assert association_object.updated_at > association_out.updated_at
|
||||
assert association_object.deleted_at is not None
|
||||
AssociationsTestingService().assert_deleted(
|
||||
pk=association_out.pk,
|
||||
reference=association_out,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -65,6 +66,34 @@ def test_ok_htmx(authenticated_client: Client,
|
|||
assert result.json() == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ok_archived(authenticated_client: Client,
|
||||
archived_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.delete', args=(archived_association_out.pk,)),
|
||||
data={
|
||||
'canhazconfirm': 'hai',
|
||||
},
|
||||
)
|
||||
|
||||
# Then
|
||||
asserts.assertRedirects(
|
||||
result,
|
||||
reverse(
|
||||
'ui.associations.browse',
|
||||
query=[('mode', AssociationsSearchMode.ARCHIVED.value)],
|
||||
),
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
AssociationsTestingService().assert_deleted(
|
||||
pk=archived_association_out.pk,
|
||||
reference=archived_association_out,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invalid_all_missing(authenticated_client: Client,
|
||||
association_out,
|
||||
|
@ -78,13 +107,13 @@ def test_invalid_all_missing(authenticated_client: Client,
|
|||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.OK
|
||||
|
||||
association_object = Association.objects.get(pk=association_out.pk)
|
||||
assert association_object.updated_at == association_out.updated_at
|
||||
assert association_object.deleted_at is None
|
||||
|
||||
assert 'canhazconfirm' in result.context['form'].errors
|
||||
|
||||
AssociationsTestingService().assert_not_deleted(
|
||||
pk=association_out.pk,
|
||||
reference=association_out,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invalid_all_empty(authenticated_client: Client,
|
||||
|
@ -100,13 +129,45 @@ def test_invalid_all_empty(authenticated_client: Client,
|
|||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.OK
|
||||
|
||||
association_object = Association.objects.get(pk=association_out.pk)
|
||||
assert association_object.updated_at == association_out.updated_at
|
||||
assert association_object.deleted_at is None
|
||||
|
||||
assert 'canhazconfirm' in result.context['form'].errors
|
||||
|
||||
AssociationsTestingService().assert_not_deleted(
|
||||
pk=association_out.pk,
|
||||
reference=association_out,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_deleted(authenticated_client: Client,
|
||||
deleted_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.delete', args=(deleted_association_out.pk,)),
|
||||
data={
|
||||
'canhazconfirm': 'hai',
|
||||
},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_found(authenticated_client: Client,
|
||||
null_uuid,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.delete', args=(null_uuid,)),
|
||||
data={
|
||||
'canhazconfirm': 'hai',
|
||||
},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_other_account_association(authenticated_client: Client,
|
||||
|
|
|
@ -47,10 +47,10 @@ def test_ok(authenticated_client: Client,
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invalid_all_empty(authenticated_client: Client,
|
||||
association_out,
|
||||
payload,
|
||||
):
|
||||
def test_all_empty(authenticated_client: Client,
|
||||
association_out,
|
||||
payload,
|
||||
):
|
||||
# Given
|
||||
effective_payload = {
|
||||
key: ''
|
||||
|
@ -79,9 +79,9 @@ def test_invalid_all_empty(authenticated_client: Client,
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invalid_all_missing(authenticated_client: Client,
|
||||
association_out,
|
||||
):
|
||||
def test_all_missing(authenticated_client: Client,
|
||||
association_out,
|
||||
):
|
||||
# Given
|
||||
effective_payload = {}
|
||||
|
||||
|
@ -105,6 +105,45 @@ def test_invalid_all_missing(authenticated_client: Client,
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_archived(authenticated_client: Client,
|
||||
archived_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.edit', args=(archived_association_out.pk,)),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_deleted(authenticated_client: Client,
|
||||
deleted_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.edit', args=(deleted_association_out.pk,)),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_found(authenticated_client: Client,
|
||||
null_uuid,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.edit', args=(null_uuid,)),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_other_account_association(authenticated_client: Client,
|
||||
other_account_association_out,
|
||||
|
|
|
@ -128,6 +128,54 @@ def test_invalid_all_empty(authenticated_client: Client,
|
|||
assert 'canhazconfirm' in result.context['form'].errors
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_archived(authenticated_client: Client,
|
||||
archived_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.refresh', args=(archived_association_out.pk,)),
|
||||
data={
|
||||
'canhazconfirm': 'hai',
|
||||
},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_deleted(authenticated_client: Client,
|
||||
deleted_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.refresh', args=(deleted_association_out.pk,)),
|
||||
data={
|
||||
'canhazconfirm': 'hai',
|
||||
},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_found(authenticated_client: Client,
|
||||
null_uuid,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.post(
|
||||
reverse('ui.associations.refresh', args=(null_uuid,)),
|
||||
data={
|
||||
'canhazconfirm': 'hai',
|
||||
},
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_other_account_association(authenticated_client: Client,
|
||||
other_account_association_out,
|
||||
|
|
|
@ -54,6 +54,45 @@ def test_ok_htmx(authenticated_client: Client,
|
|||
assert result.context['association'].target.pk == association_out.target.pk
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_archived(authenticated_client: Client,
|
||||
archived_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.get(
|
||||
reverse('ui.associations.star', args=(archived_association_out.pk,)),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_deleted(authenticated_client: Client,
|
||||
deleted_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.get(
|
||||
reverse('ui.associations.star', args=(deleted_association_out.pk,)),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_found(authenticated_client: Client,
|
||||
null_uuid,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.get(
|
||||
reverse('ui.associations.star', args=(null_uuid,)),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_other_account_association(authenticated_client: Client,
|
||||
other_account_association_out,
|
||||
|
|
|
@ -54,6 +54,45 @@ def test_ok_htmx(authenticated_client: Client,
|
|||
assert result.context['association'].target.pk == starred_association_out.target.pk
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_archived(authenticated_client: Client,
|
||||
archived_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.get(
|
||||
reverse('ui.associations.unstar', args=(archived_association_out.pk,)),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_deleted(authenticated_client: Client,
|
||||
deleted_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.get(
|
||||
reverse('ui.associations.unstar', args=(deleted_association_out.pk,)),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_found(authenticated_client: Client,
|
||||
null_uuid,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.get(
|
||||
reverse('ui.associations.unstar', args=(null_uuid,)),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_other_account_association(authenticated_client: Client,
|
||||
other_account_starred_association_out,
|
||||
|
|
|
@ -66,6 +66,19 @@ def test_authenticated_deleted(authenticated_client: Client,
|
|||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_authenticated_deleted_save(authenticated_client: Client,
|
||||
deleted_save_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.get(
|
||||
reverse('ui.associations.view', args=(deleted_save_association_out.pk,)),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_authenticated_not_found(authenticated_client: Client,
|
||||
null_uuid,
|
||||
|
@ -169,6 +182,23 @@ def test_authenticated_share_deleted(authenticated_client: Client,
|
|||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_authenticated_share_deleted_save(authenticated_client: Client,
|
||||
other_account_deleted_save_association_out,
|
||||
):
|
||||
# When
|
||||
result = authenticated_client.get(
|
||||
reverse(
|
||||
'ui.associations.view',
|
||||
args=(other_account_deleted_save_association_out.pk,),
|
||||
query=[('share', 'true')],
|
||||
),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_authenticated_share_not_found(authenticated_client: Client,
|
||||
null_uuid,
|
||||
|
@ -240,6 +270,23 @@ def test_anonymous_share_deleted(client: Client,
|
|||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_anonymous_share_deleted_save(client: Client,
|
||||
deleted_save_association_out,
|
||||
):
|
||||
# When
|
||||
result = client.get(
|
||||
reverse(
|
||||
'ui.associations.view',
|
||||
args=(deleted_save_association_out.pk,),
|
||||
query=[('share', 'true')],
|
||||
),
|
||||
)
|
||||
|
||||
# Then
|
||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_anonymous_share_not_found(client: Client,
|
||||
null_uuid,
|
||||
|
|
|
@ -76,6 +76,7 @@ def test_ok(override_settings_upload_path,
|
|||
|
||||
mock_ui_import_from_pocket_task_apply_async.assert_called_once_with(
|
||||
kwargs={
|
||||
'job': mock.ANY,
|
||||
'account_uuid': account.pk,
|
||||
'csv_path': str(uploaded_file_path),
|
||||
},
|
||||
|
|
|
@ -77,10 +77,11 @@ def test_auth_key_not_found(null_uuid,
|
|||
|
||||
call_result = result.json()
|
||||
assert 'error' in call_result
|
||||
assert call_result['error']['data'].startswith(
|
||||
assert call_result['error']['code'] == -32001
|
||||
assert call_result['error']['message'].startswith(
|
||||
'Auth Key not found',
|
||||
)
|
||||
assert call_auth_key in call_result['error']['data']
|
||||
assert call_auth_key in call_result['error']['message']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -108,10 +109,11 @@ def test_deleted_auth_key(deleted_auth_key_out,
|
|||
|
||||
call_result = result.json()
|
||||
assert 'error' in call_result
|
||||
assert call_result['error']['data'].startswith(
|
||||
assert call_result['error']['code'] == -32001
|
||||
assert call_result['error']['message'].startswith(
|
||||
'Auth Key not found',
|
||||
)
|
||||
assert call_auth_key in call_result['error']['data']
|
||||
assert call_auth_key in call_result['error']['message']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -139,10 +141,11 @@ def test_expired_auth_key(expired_auth_key_out,
|
|||
|
||||
call_result = result.json()
|
||||
assert 'error' in call_result
|
||||
assert call_result['error']['data'].startswith(
|
||||
assert call_result['error']['code'] == -32000
|
||||
assert call_result['error']['message'].startswith(
|
||||
'Auth Key expired',
|
||||
)
|
||||
assert call_auth_key in call_result['error']['data']
|
||||
assert call_auth_key in call_result['error']['message']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -170,10 +173,11 @@ def test_consumed_auth_key(consumed_auth_key,
|
|||
|
||||
call_result = result.json()
|
||||
assert 'error' in call_result
|
||||
assert call_result['error']['data'].startswith(
|
||||
assert call_result['error']['code'] == -32000
|
||||
assert call_result['error']['message'].startswith(
|
||||
'Auth Key already consumed',
|
||||
)
|
||||
assert call_auth_key in call_result['error']['data']
|
||||
assert call_auth_key in call_result['error']['message']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -201,4 +205,5 @@ def test_inactive_account(inactive_account_auth_key,
|
|||
|
||||
call_result = result.json()
|
||||
assert 'error' in call_result
|
||||
assert str(inactive_account.pk) in call_result['error']['data']
|
||||
assert call_result['error']['code'] == -32001
|
||||
assert str(inactive_account.pk) in call_result['error']['message']
|
||||
|
|
|
@ -8,7 +8,7 @@ ARG APP_USER_UID
|
|||
ARG APP_USER_GID
|
||||
ARG IMAGE_ID
|
||||
|
||||
COPY --chown=$APP_USER_UID:$APP_USER_GID extension/ops/bin/*.sh /srv/bin/
|
||||
# COPY --chown=$APP_USER_UID:$APP_USER_GID extension/ops/bin/*.sh /srv/bin/
|
||||
|
||||
VOLUME ["/srv/node_modules", "/srv/venv"]
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ ARG APP_USER_UID
|
|||
ARG APP_USER_GID
|
||||
ARG IMAGE_ID
|
||||
|
||||
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ops/bin/*.sh /srv/bin/
|
||||
# COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ops/bin/*.sh /srv/bin/
|
||||
|
||||
VOLUME ["/srv/node_modules", "/srv/venv"]
|
||||
|
||||
|
|
11
services/packages/soa/hotpocket_soa/constants.py
Normal file
11
services/packages/soa/hotpocket_soa/constants.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class BackendServiceErrorCode(enum.Enum):
|
||||
INTERNAL = -32000
|
||||
NOT_FOUND = -32001
|
||||
ACCESS_DENIED = -32002
|
||||
INVALID = -32003
|
89
services/packages/soa/hotpocket_soa/exceptions/backend.py
Normal file
89
services/packages/soa/hotpocket_soa/exceptions/backend.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from hotpocket_soa.constants import BackendServiceErrorCode
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
VALIDATION_CODE_INVALID = 'invalid'
|
||||
|
||||
|
||||
def get_validation_error_data(validation_error: typing.Any) -> typing.Any: # Heh
|
||||
if hasattr(validation_error, 'error_dict') is True:
|
||||
return {
|
||||
field: [
|
||||
get_validation_error_data(inner_error)
|
||||
for inner_error
|
||||
in inner_errors
|
||||
]
|
||||
for field, inner_errors in validation_error.error_dict.items()
|
||||
}
|
||||
elif hasattr(validation_error, 'error_list') is True and len(validation_error.error_list) > 1:
|
||||
return [
|
||||
get_validation_error_data(inner_error)
|
||||
for inner_error
|
||||
in validation_error.error_list
|
||||
]
|
||||
elif hasattr(validation_error, 'code') is True:
|
||||
return validation_error.code
|
||||
elif isinstance(validation_error, (tuple, list)) is True:
|
||||
return [
|
||||
get_validation_error_data(inner_error)
|
||||
for inner_error
|
||||
in validation_error
|
||||
]
|
||||
elif isinstance(validation_error, dict) is True:
|
||||
return {
|
||||
field: [
|
||||
get_validation_error_data(inner_error)
|
||||
for inner_error
|
||||
in inner_errors
|
||||
]
|
||||
for field, inner_errors in validation_error.items()
|
||||
}
|
||||
else:
|
||||
return VALIDATION_CODE_INVALID
|
||||
|
||||
|
||||
class BackendServiceError(Exception):
|
||||
CODE = BackendServiceErrorCode.INTERNAL
|
||||
|
||||
def __init__(self, message: str, *args):
|
||||
super().__init__(message, *args)
|
||||
self.message = message
|
||||
|
||||
self.data: typing.Any = None
|
||||
if len(args) > 0:
|
||||
self.data = args[0]
|
||||
|
||||
|
||||
class InternalError(BackendServiceError):
|
||||
pass
|
||||
|
||||
|
||||
class NotFound(BackendServiceError):
|
||||
CODE = BackendServiceErrorCode.NOT_FOUND
|
||||
|
||||
|
||||
class AccessDenied(BackendServiceError):
|
||||
CODE = BackendServiceErrorCode.ACCESS_DENIED
|
||||
|
||||
|
||||
class Invalid(BackendServiceError):
|
||||
CODE = BackendServiceErrorCode.INVALID
|
||||
|
||||
@classmethod
|
||||
def from_django_validation_error(cls: type[typing.Self],
|
||||
exception: ValidationError,
|
||||
message: str | None = None,
|
||||
) -> typing.Self:
|
||||
data = get_validation_error_data(exception)
|
||||
|
||||
result = cls(message or 'Invalid', data)
|
||||
result.__cause__ = exception
|
||||
|
||||
return result
|
28
services/packages/soa/hotpocket_soa/exceptions/frontend.py
Normal file
28
services/packages/soa/hotpocket_soa/exceptions/frontend.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .backend import BackendServiceError
|
||||
|
||||
|
||||
class SOAError(Exception):
|
||||
def __init__(self, code: int, message: str, *args):
|
||||
super().__init__(code, message, *args)
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
self.data: typing.Any = None
|
||||
if len(args) > 0:
|
||||
self.data = args[0]
|
||||
|
||||
@classmethod
|
||||
def from_backend_error(cls: type[typing.Self], exception: BackendServiceError) -> typing.Self:
|
||||
result = cls(
|
||||
exception.CODE.value,
|
||||
exception.message,
|
||||
exception.data,
|
||||
)
|
||||
result.__cause__ = exception
|
||||
|
||||
return result
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import http
|
||||
import uuid
|
||||
|
||||
from hotpocket_backend.apps.accounts.services import (
|
||||
|
@ -11,6 +12,7 @@ from hotpocket_soa.dto.accounts import (
|
|||
AccessTokenOut,
|
||||
AccessTokensQuery,
|
||||
)
|
||||
from hotpocket_soa.exceptions.backend import NotFound
|
||||
|
||||
from .base import ProxyService, SOAError
|
||||
|
||||
|
@ -19,22 +21,18 @@ class AccessTokensService(ProxyService):
|
|||
class AccessTokensServiceError(SOAError):
|
||||
pass
|
||||
|
||||
class AccessTokenNotFound(AccessTokensServiceError):
|
||||
class NotFound(AccessTokensServiceError):
|
||||
pass
|
||||
|
||||
class AccessTokenAccessDenied(AccessTokensServiceError):
|
||||
class AccessDenied(AccessTokensServiceError):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.backend_access_tokens_service = BackendAccessTokensService()
|
||||
|
||||
def wrap_exception(self, exception: Exception) -> Exception:
|
||||
new_exception_args = []
|
||||
if len(exception.args) > 0:
|
||||
new_exception_args = [exception.args[0]]
|
||||
|
||||
return self.AccessTokensServiceError(*new_exception_args)
|
||||
def get_error_class(self) -> type[SOAError]:
|
||||
return self.AccessTokensServiceError
|
||||
|
||||
def create(self,
|
||||
*,
|
||||
|
@ -69,16 +67,14 @@ class AccessTokensService(ProxyService):
|
|||
)
|
||||
|
||||
if result.account_uuid != account_uuid:
|
||||
raise self.AccessTokenAccessDenied(
|
||||
raise self.AccessDenied(
|
||||
http.HTTPStatus.FORBIDDEN.value,
|
||||
f'account_uuid=`{account_uuid}` pk=`{pk}`',
|
||||
)
|
||||
|
||||
return result
|
||||
except SOAError as exception:
|
||||
if isinstance(exception.__cause__, BackendAccessTokensService.AccessTokenNotFound) is True:
|
||||
raise self.AccessTokenNotFound(*exception.args) from exception
|
||||
else:
|
||||
raise
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
||||
def get_by_key(self,
|
||||
*,
|
||||
|
@ -96,16 +92,14 @@ class AccessTokensService(ProxyService):
|
|||
)
|
||||
|
||||
if result.account_uuid != account_uuid:
|
||||
raise self.AccessTokenAccessDenied(
|
||||
raise self.AccessDenied(
|
||||
http.HTTPStatus.FORBIDDEN.value,
|
||||
f'account_uuid=`{account_uuid}` key=`{key}`',
|
||||
)
|
||||
|
||||
return result
|
||||
except SOAError as exception:
|
||||
if isinstance(exception.__cause__, BackendAccessTokensService.AccessTokenNotFound) is True:
|
||||
raise self.AccessTokenNotFound(f'account_uuid=`{account_uuid}` pk=`{key}`') from exception
|
||||
else:
|
||||
raise
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
||||
def search(self,
|
||||
*,
|
||||
|
@ -124,23 +118,29 @@ class AccessTokensService(ProxyService):
|
|||
]
|
||||
|
||||
def delete(self, *, access_token: AccessTokenOut) -> bool:
|
||||
return self.call(
|
||||
self.backend_access_tokens_service,
|
||||
'delete',
|
||||
pk=access_token.pk,
|
||||
)
|
||||
try:
|
||||
return self.call(
|
||||
self.backend_access_tokens_service,
|
||||
'delete',
|
||||
pk=access_token.pk,
|
||||
)
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
||||
def update_meta(self,
|
||||
*,
|
||||
access_token: AccessTokenOut,
|
||||
update: AccessTokenMetaUpdateIn,
|
||||
) -> AccessTokenOut:
|
||||
return AccessTokenOut.model_validate(
|
||||
self.call(
|
||||
self.backend_access_tokens_service,
|
||||
'update_meta',
|
||||
pk=access_token.pk,
|
||||
update=update,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
try:
|
||||
return AccessTokenOut.model_validate(
|
||||
self.call(
|
||||
self.backend_access_tokens_service,
|
||||
'update_meta',
|
||||
pk=access_token.pk,
|
||||
update=update,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
|
|
@ -7,6 +7,7 @@ from hotpocket_backend.apps.accounts.services import (
|
|||
AccountsService as BackendAccountsService,
|
||||
)
|
||||
from hotpocket_soa.dto.accounts import AccountOut
|
||||
from hotpocket_soa.exceptions.backend import NotFound
|
||||
|
||||
from .base import ProxyService, SOAError
|
||||
|
||||
|
@ -15,19 +16,15 @@ class AccountsService(ProxyService):
|
|||
class AccountsServiceError(SOAError):
|
||||
pass
|
||||
|
||||
class AccountNotFound(AccountsServiceError):
|
||||
class NotFound(AccountsServiceError):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.backend_accounts_service = BackendAccountsService()
|
||||
|
||||
def wrap_exception(self, exception: Exception) -> Exception:
|
||||
new_exception_args = []
|
||||
if len(exception.args) > 0:
|
||||
new_exception_args = [exception.args[0]]
|
||||
|
||||
return self.AccountsServiceError(*new_exception_args)
|
||||
def get_error_class(self) -> type[SOAError]:
|
||||
return self.AccountsServiceError
|
||||
|
||||
def get(self, *, pk: uuid.UUID) -> AccountOut:
|
||||
try:
|
||||
|
@ -41,8 +38,5 @@ class AccountsService(ProxyService):
|
|||
)
|
||||
|
||||
return result
|
||||
except SOAError as exception:
|
||||
if isinstance(exception.__cause__, BackendAccountsService.AccountNotFound) is True:
|
||||
raise self.AccountNotFound(*exception.args) from exception
|
||||
else:
|
||||
raise
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import http
|
||||
import uuid
|
||||
|
||||
from hotpocket_backend.apps.saves.services import (
|
||||
|
@ -14,6 +15,7 @@ from hotpocket_soa.dto.associations import (
|
|||
AssociationWithTargetOut,
|
||||
)
|
||||
from hotpocket_soa.dto.saves import SaveOut
|
||||
from hotpocket_soa.exceptions.backend import NotFound
|
||||
|
||||
from .base import ProxyService, SOAError
|
||||
|
||||
|
@ -22,22 +24,18 @@ class AssociationsService(ProxyService):
|
|||
class AssociationsServiceError(SOAError):
|
||||
pass
|
||||
|
||||
class AssociationNotFound(AssociationsServiceError):
|
||||
class NotFound(AssociationsServiceError):
|
||||
pass
|
||||
|
||||
class AssociationAccessDenied(AssociationsServiceError):
|
||||
class AccessDenied(AssociationsServiceError):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.backend_associations_service = BackendAssociationsService()
|
||||
|
||||
def wrap_exception(self, exception: Exception) -> Exception:
|
||||
new_exception_args = []
|
||||
if len(exception.args) > 0:
|
||||
new_exception_args = [exception.args[0]]
|
||||
|
||||
return self.AssociationsServiceError(*new_exception_args)
|
||||
def get_error_class(self) -> type[SOAError]:
|
||||
return self.AssociationsServiceError
|
||||
|
||||
def create(self,
|
||||
*,
|
||||
|
@ -81,19 +79,19 @@ class AssociationsService(ProxyService):
|
|||
)
|
||||
|
||||
if allow_archived is False and result.archived_at is not None:
|
||||
raise self.AssociationNotFound(f'pk=`{pk}`')
|
||||
raise self.NotFound(
|
||||
http.HTTPStatus.NOT_FOUND.value, f'pk=`{pk}`',
|
||||
)
|
||||
|
||||
if account_uuid is not None and result.account_uuid != account_uuid:
|
||||
raise self.AssociationAccessDenied(
|
||||
raise self.AccessDenied(
|
||||
http.HTTPStatus.FORBIDDEN.value,
|
||||
f'account_uuid=`{account_uuid}` pk=`{pk}`',
|
||||
)
|
||||
|
||||
return result
|
||||
except SOAError as exception:
|
||||
if isinstance(exception.__cause__, BackendAssociationsService.AssociationNotFound) is True:
|
||||
raise self.AssociationNotFound(*exception.args) from exception
|
||||
else:
|
||||
raise
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
||||
def search(self,
|
||||
*,
|
||||
|
@ -112,50 +110,65 @@ class AssociationsService(ProxyService):
|
|||
]
|
||||
|
||||
def archive(self, *, association: AssociationOut) -> bool:
|
||||
return self.call(
|
||||
self.backend_associations_service,
|
||||
'archive',
|
||||
pk=association.pk,
|
||||
)
|
||||
try:
|
||||
return self.call(
|
||||
self.backend_associations_service,
|
||||
'archive',
|
||||
pk=association.pk,
|
||||
)
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
||||
def star(self, *, association: AssociationOut) -> AssociationOut:
|
||||
return AssociationOut.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'star',
|
||||
pk=association.pk,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
try:
|
||||
return AssociationOut.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'star',
|
||||
pk=association.pk,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
||||
def unstar(self, *, association: AssociationOut) -> AssociationOut:
|
||||
return AssociationOut.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'unstar',
|
||||
pk=association.pk,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
try:
|
||||
return AssociationOut.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'unstar',
|
||||
pk=association.pk,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
||||
def update(self,
|
||||
*,
|
||||
association: AssociationOut,
|
||||
update: AssociationUpdateIn,
|
||||
) -> AssociationOut:
|
||||
return AssociationOut.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'update',
|
||||
pk=association.pk,
|
||||
update=update,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
try:
|
||||
return AssociationOut.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'update',
|
||||
pk=association.pk,
|
||||
update=update,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
||||
def delete(self, *, association: AssociationOut) -> bool:
|
||||
return self.call(
|
||||
self.backend_associations_service,
|
||||
'delete',
|
||||
pk=association.pk,
|
||||
)
|
||||
try:
|
||||
return self.call(
|
||||
self.backend_associations_service,
|
||||
'delete',
|
||||
pk=association.pk,
|
||||
)
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import http
|
||||
import uuid
|
||||
|
||||
from hotpocket_backend.apps.accounts.services import (
|
||||
AuthKeysService as BackendAuthKeysService,
|
||||
)
|
||||
from hotpocket_soa.dto.accounts import AuthKeyOut
|
||||
from hotpocket_soa.exceptions.backend import NotFound
|
||||
|
||||
from .base import ProxyService, SOAError
|
||||
|
||||
|
@ -15,23 +17,16 @@ class AuthKeysService(ProxyService):
|
|||
class AuthKeysServiceError(SOAError):
|
||||
pass
|
||||
|
||||
class AuthKeyNotFound(AuthKeysServiceError):
|
||||
class NotFound(AuthKeysServiceError):
|
||||
pass
|
||||
|
||||
class AuthKeyAccessDenied(AuthKeysServiceError):
|
||||
class AccessDenied(AuthKeysServiceError):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.backend_auth_keys_service = BackendAuthKeysService()
|
||||
|
||||
def wrap_exception(self, exception: Exception) -> Exception:
|
||||
new_exception_args = []
|
||||
if len(exception.args) > 0:
|
||||
new_exception_args = [exception.args[0]]
|
||||
|
||||
return self.AuthKeysServiceError(*new_exception_args)
|
||||
|
||||
def _check_auth_key_access(self,
|
||||
auth_key: AuthKeyOut,
|
||||
account_uuid: uuid.UUID | None,
|
||||
|
@ -70,16 +65,14 @@ class AuthKeysService(ProxyService):
|
|||
)
|
||||
|
||||
if self._check_auth_key_access(result, account_uuid) is False:
|
||||
raise self.AuthKeyAccessDenied(
|
||||
raise self.AccessDenied(
|
||||
http.HTTPStatus.FORBIDDEN.value,
|
||||
f'account_uuid=`{account_uuid}` pk=`{pk}`',
|
||||
)
|
||||
|
||||
return result
|
||||
except SOAError as exception:
|
||||
if isinstance(exception.__cause__, BackendAuthKeysService.AuthKeyNotFound) is True:
|
||||
raise self.AuthKeyNotFound(*exception.args) from exception
|
||||
else:
|
||||
raise
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
||||
def get_by_key(self,
|
||||
*,
|
||||
|
@ -97,13 +90,11 @@ class AuthKeysService(ProxyService):
|
|||
)
|
||||
|
||||
if self._check_auth_key_access(result, account_uuid) is False:
|
||||
raise self.AuthKeyAccessDenied(
|
||||
raise self.AccessDenied(
|
||||
http.HTTPStatus.FORBIDDEN.value,
|
||||
f'account_uuid=`{account_uuid}` key=`{key}`',
|
||||
)
|
||||
|
||||
return result
|
||||
except SOAError as exception:
|
||||
if isinstance(exception.__cause__, BackendAuthKeysService.AuthKeyNotFound) is True:
|
||||
raise self.AuthKeyNotFound(*exception.args) from exception
|
||||
else:
|
||||
raise
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
|
|
@ -1,16 +1,66 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import http
|
||||
import types
|
||||
import typing
|
||||
|
||||
|
||||
class SOAError(Exception):
|
||||
pass
|
||||
from hotpocket_soa.exceptions.backend import BackendServiceError
|
||||
from hotpocket_soa.exceptions.frontend import SOAError
|
||||
|
||||
|
||||
class Service:
|
||||
def wrap_exception(self, exception: Exception) -> Exception:
|
||||
return SOAError(exception.args[0])
|
||||
def __getattribute__(self, name: str) -> typing.Any:
|
||||
result = super().__getattribute__(name)
|
||||
|
||||
is_service_method = all((
|
||||
name.startswith('_') is False,
|
||||
hasattr(Service, name) is False,
|
||||
isinstance(result, types.MethodType),
|
||||
getattr(result, '__self__', None) is self,
|
||||
))
|
||||
if is_service_method is True:
|
||||
@functools.wraps(result)
|
||||
def wrapped_result(*args, **kwargs):
|
||||
try:
|
||||
return result(*args, **kwargs)
|
||||
except Exception as exception:
|
||||
raise self.wrap_exception(exception) from exception
|
||||
|
||||
return wrapped_result
|
||||
|
||||
return result
|
||||
|
||||
def get_error_class(self) -> type[SOAError]:
|
||||
return SOAError
|
||||
|
||||
def wrap_exception(self, exception: Exception) -> SOAError:
|
||||
error_class = self.get_error_class()
|
||||
|
||||
if isinstance(exception, error_class) is True:
|
||||
return typing.cast(SOAError, exception)
|
||||
|
||||
result = error_class(
|
||||
http.HTTPStatus.IM_A_TEAPOT.value,
|
||||
'SOA Error',
|
||||
*exception.args,
|
||||
)
|
||||
|
||||
if isinstance(exception, BackendServiceError) is True:
|
||||
baskend_error = typing.cast(BackendServiceError, exception)
|
||||
result = error_class(
|
||||
baskend_error.CODE.value,
|
||||
baskend_error.message,
|
||||
baskend_error.data,
|
||||
*baskend_error.args,
|
||||
)
|
||||
|
||||
result.__cause__ = exception
|
||||
return result
|
||||
|
||||
def call(self, *args, **kwargs) -> typing.Any:
|
||||
raise NotImplementedError('TODO')
|
||||
|
||||
|
||||
class ProxyService(Service):
|
||||
|
@ -18,7 +68,4 @@ class ProxyService(Service):
|
|||
handler = getattr(service, method, None)
|
||||
assert handler is not None, f'Unknown method: method=`{method}`'
|
||||
|
||||
try:
|
||||
return handler(*args, **kwargs)
|
||||
except Exception as exception:
|
||||
raise self.wrap_exception(exception) from exception
|
||||
return handler(*args, **kwargs)
|
||||
|
|
|
@ -15,12 +15,8 @@ class BotService(ProxyService):
|
|||
super().__init__()
|
||||
self.backend_associations_service = BackendBotService()
|
||||
|
||||
def wrap_exception(self, exception: Exception) -> Exception:
|
||||
new_exception_args = []
|
||||
if len(exception.args) > 0:
|
||||
new_exception_args = [exception.args[0]]
|
||||
|
||||
return self.BotServiceError(*new_exception_args)
|
||||
def get_error_class(self) -> type[SOAError]:
|
||||
return self.BotServiceError
|
||||
|
||||
def is_netloc_banned(self, *, url: str) -> bool:
|
||||
return self.call(
|
||||
|
|
|
@ -18,17 +18,13 @@ class SaveProcessorService(ProxyService):
|
|||
class SaveProcessorServiceError(SOAError):
|
||||
pass
|
||||
|
||||
def wrap_exception(self, exception: Exception) -> Exception:
|
||||
new_exception_args = []
|
||||
if len(exception.args) > 0:
|
||||
new_exception_args = [exception.args[0]]
|
||||
|
||||
return self.SaveProcessorServiceError(*new_exception_args)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.backend_save_processor_service = BackendSaveProcessorService()
|
||||
|
||||
def get_error_class(self) -> type[SOAError]:
|
||||
return self.SaveProcessorServiceError
|
||||
|
||||
def schedule_process_save(self, *, save: SaveOut) -> AsyncResultOut:
|
||||
result = AsyncResultOut.model_validate(
|
||||
self.call(
|
||||
|
|
|
@ -7,6 +7,7 @@ from hotpocket_backend.apps.saves.services import (
|
|||
SavesService as BackendSavesService,
|
||||
)
|
||||
from hotpocket_soa.dto.saves import SaveIn, SaveOut
|
||||
from hotpocket_soa.exceptions.backend import Invalid, NotFound
|
||||
|
||||
from .base import ProxyService, SOAError
|
||||
|
||||
|
@ -15,30 +16,34 @@ class SavesService(ProxyService):
|
|||
class SavesServiceError(SOAError):
|
||||
pass
|
||||
|
||||
class SaveNotFound(SavesServiceError):
|
||||
class NotFound(SavesServiceError):
|
||||
pass
|
||||
|
||||
def wrap_exception(self, exception: Exception) -> Exception:
|
||||
new_exception_args = []
|
||||
if len(exception.args) > 0:
|
||||
new_exception_args = [exception.args[0]]
|
||||
|
||||
return self.SavesServiceError(*new_exception_args)
|
||||
class Invalid(SavesServiceError):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.backend_saves_service = BackendSavesService()
|
||||
|
||||
def get_error_class(self) -> type[SOAError]:
|
||||
return self.SavesServiceError
|
||||
|
||||
def create(self, *, account_uuid: uuid.UUID, save: SaveIn) -> SaveOut:
|
||||
return SaveOut.model_validate(
|
||||
self.call(
|
||||
self.backend_saves_service,
|
||||
'create',
|
||||
account_uuid=account_uuid,
|
||||
save=save,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
try:
|
||||
return SaveOut.model_validate(
|
||||
self.call(
|
||||
self.backend_saves_service,
|
||||
'create',
|
||||
account_uuid=account_uuid,
|
||||
save=save,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
except Invalid as exception:
|
||||
raise self.Invalid(
|
||||
exception.CODE.value, exception.message, exception.data,
|
||||
)
|
||||
|
||||
def get(self, *, pk: uuid.UUID) -> SaveOut:
|
||||
try:
|
||||
|
@ -52,8 +57,5 @@ class SavesService(ProxyService):
|
|||
)
|
||||
|
||||
return result
|
||||
except SOAError as exception:
|
||||
if isinstance(exception.__cause__, BackendSavesService.SaveNotFound) is True:
|
||||
raise self.SaveNotFound(*exception.args) from exception
|
||||
else:
|
||||
raise
|
||||
except NotFound as exception:
|
||||
raise self.NotFound.from_backend_error(exception)
|
||||
|
|
|
@ -24,8 +24,8 @@ server {
|
|||
listen *:443 ssl;
|
||||
server_name app.hotpocket.work.bthlabs.net;
|
||||
|
||||
ssl_certificate /Users/bilbo/Projects/PLAYG/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.crt;
|
||||
ssl_certificate_key /Users/bilbo/Projects/PLAYG/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.key;
|
||||
ssl_certificate /Users/bilbo/Projects/HOTPOCKET/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.crt;
|
||||
ssl_certificate_key /Users/bilbo/Projects/HOTPOCKET/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.key;
|
||||
|
||||
location / {
|
||||
# proxy_cache_bypass $http_upgrade;
|
||||
|
@ -71,8 +71,8 @@ server {
|
|||
listen *:443 ssl;
|
||||
server_name admin.hotpocket.work.bthlabs.net;
|
||||
|
||||
ssl_certificate /Users/bilbo/Projects/PLAYG/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.crt;
|
||||
ssl_certificate_key /Users/bilbo/Projects/PLAYG/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.key;
|
||||
ssl_certificate /Users/bilbo/Projects/HOTPOCKET/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.crt;
|
||||
ssl_certificate_key /Users/bilbo/Projects/HOTPOCKET/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.key;
|
||||
|
||||
location / {
|
||||
# proxy_cache_bypass $http_upgrade;
|
||||
|
|
Loading…
Reference in New Issue
Block a user