You've already forked hotpocket
This commit is contained in:
3
services/packages/soa/README.md
Normal file
3
services/packages/soa/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# HotPocket by BTHLabs
|
||||
|
||||
This repository contains the _HotPocket SOA_ project.
|
||||
0
services/packages/soa/hotpocket_soa/__init__.py
Normal file
0
services/packages/soa/hotpocket_soa/__init__.py
Normal file
2
services/packages/soa/hotpocket_soa/dto/__init__.py
Normal file
2
services/packages/soa/hotpocket_soa/dto/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .bot import BotResultOut # noqa: F401
|
||||
from .saves import SaveIn, SaveOut # noqa: F401
|
||||
76
services/packages/soa/hotpocket_soa/dto/associations.py
Normal file
76
services/packages/soa/hotpocket_soa/dto/associations.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
import pydantic
|
||||
|
||||
from hotpocket_common.constants import AssociationsSearchMode
|
||||
|
||||
from .base import ModelOut, Query
|
||||
from .saves import SaveOut
|
||||
|
||||
|
||||
class AssociationOut(ModelOut):
|
||||
archived_at: datetime.datetime | None
|
||||
starred_at: datetime.datetime | None
|
||||
target_uuid: uuid.UUID = pydantic.Field(alias='target_id')
|
||||
target_meta: dict
|
||||
target_title: str | None
|
||||
target_description: str | None
|
||||
|
||||
@property
|
||||
def is_starred(self) -> bool:
|
||||
return self.starred_at is not None
|
||||
|
||||
def get_title(self) -> str | None:
|
||||
return (
|
||||
self.target_title
|
||||
if self.target_title is not None
|
||||
else self.target_meta.get('title', None)
|
||||
)
|
||||
|
||||
def get_description(self) -> str | None:
|
||||
return (
|
||||
self.target_description
|
||||
if self.target_description is not None
|
||||
else self.target_meta.get('description', None)
|
||||
)
|
||||
|
||||
@property
|
||||
def title(self) -> str | None:
|
||||
return self.get_title()
|
||||
|
||||
@property
|
||||
def description(self) -> str | None:
|
||||
return self.get_description()
|
||||
|
||||
|
||||
class AssociationWithTargetOut(AssociationOut):
|
||||
target: SaveOut
|
||||
|
||||
@property
|
||||
def title(self) -> str | None:
|
||||
return self.get_title() or self.target.title
|
||||
|
||||
@property
|
||||
def description(self) -> str | None:
|
||||
return self.get_description() or self.target.description
|
||||
|
||||
|
||||
class AssociationIn(pydantic.BaseModel):
|
||||
archived_at: datetime.datetime | None
|
||||
|
||||
|
||||
class AssociationsQuery(Query):
|
||||
account_uuid: uuid.UUID
|
||||
before: uuid.UUID | None = pydantic.Field(default=None)
|
||||
after: uuid.UUID | None = pydantic.Field(default=None)
|
||||
search: str | None = pydantic.Field(default=None)
|
||||
mode: AssociationsSearchMode = pydantic.Field(default=AssociationsSearchMode.DEFAULT)
|
||||
|
||||
|
||||
class AssociationUpdateIn(pydantic.BaseModel):
|
||||
target_title: str | None = pydantic.Field(default=None)
|
||||
target_description: str | None = pydantic.Field(default=None)
|
||||
24
services/packages/soa/hotpocket_soa/dto/base.py
Normal file
24
services/packages/soa/hotpocket_soa/dto/base.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
import pydantic
|
||||
|
||||
|
||||
class ModelOut(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
account_uuid: uuid.UUID
|
||||
created_at: datetime.datetime
|
||||
updated_at: datetime.datetime
|
||||
deleted_at: datetime.datetime | None
|
||||
is_active: bool
|
||||
|
||||
@property
|
||||
def pk(self) -> uuid.UUID:
|
||||
return self.id
|
||||
|
||||
|
||||
class Query(pydantic.BaseModel):
|
||||
pass
|
||||
10
services/packages/soa/hotpocket_soa/dto/bot.py
Normal file
10
services/packages/soa/hotpocket_soa/dto/bot.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import pydantic
|
||||
|
||||
|
||||
class BotResultOut(pydantic.BaseModel):
|
||||
title: str | None
|
||||
description: str | None
|
||||
is_netloc_banned: bool
|
||||
8
services/packages/soa/hotpocket_soa/dto/celery.py
Normal file
8
services/packages/soa/hotpocket_soa/dto/celery.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import pydantic
|
||||
|
||||
|
||||
class AsyncResultOut(pydantic.BaseModel):
|
||||
id: str
|
||||
65
services/packages/soa/hotpocket_soa/dto/saves.py
Normal file
65
services/packages/soa/hotpocket_soa/dto/saves.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import urllib.parse
|
||||
|
||||
import pydantic
|
||||
|
||||
from .base import ModelOut, Query
|
||||
|
||||
|
||||
class SaveOut(ModelOut):
|
||||
key: str
|
||||
url: str
|
||||
title: str | None
|
||||
description: str | None
|
||||
last_processed_at: datetime.datetime | None
|
||||
is_netloc_banned: bool
|
||||
|
||||
@property
|
||||
def parsed_url(self) -> urllib.parse.SplitResult:
|
||||
if hasattr(self, '_parsed_url') is False:
|
||||
self._parsed_url = urllib.parse.urlsplit(self.url)
|
||||
|
||||
return self._parsed_url
|
||||
|
||||
@property
|
||||
def parsed_url_query(self) -> dict:
|
||||
if hasattr(self, '_parsed_url_query') is False:
|
||||
self._parsed_url_query = urllib.parse.parse_qs(
|
||||
self.parsed_url.query,
|
||||
)
|
||||
|
||||
return self._parsed_url_query
|
||||
|
||||
@property
|
||||
def netloc(self) -> str:
|
||||
return self.parsed_url.netloc
|
||||
|
||||
@property
|
||||
def is_youtube_video(self) -> bool:
|
||||
return any((
|
||||
all((
|
||||
self.netloc.endswith('youtube.com'),
|
||||
'v' in self.parsed_url_query,
|
||||
)),
|
||||
all((
|
||||
self.netloc.endswith('youtube.com'),
|
||||
self.parsed_url.path.startswith('/live'),
|
||||
)),
|
||||
self.netloc.endswith('youtu.be'),
|
||||
))
|
||||
|
||||
|
||||
class SaveIn(pydantic.BaseModel):
|
||||
url: str
|
||||
is_netloc_banned: bool | None = pydantic.Field(default=None)
|
||||
|
||||
|
||||
class ImportedSaveIn(SaveIn):
|
||||
title: str
|
||||
|
||||
|
||||
class SavesQuery(Query):
|
||||
pass
|
||||
0
services/packages/soa/hotpocket_soa/py.typed
Normal file
0
services/packages/soa/hotpocket_soa/py.typed
Normal file
4
services/packages/soa/hotpocket_soa/services/__init__.py
Normal file
4
services/packages/soa/hotpocket_soa/services/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .associations import AssociationsService # noqa: F401
|
||||
from .bot import BotService # noqa: F401
|
||||
from .save_processor import SaveProcessorService # noqa: F401
|
||||
from .saves import SavesService # noqa: F401
|
||||
161
services/packages/soa/hotpocket_soa/services/associations.py
Normal file
161
services/packages/soa/hotpocket_soa/services/associations.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from hotpocket_backend.apps.saves.services import (
|
||||
AssociationsService as BackendAssociationsService,
|
||||
)
|
||||
from hotpocket_soa.dto.associations import (
|
||||
AssociationOut,
|
||||
AssociationsQuery,
|
||||
AssociationUpdateIn,
|
||||
AssociationWithTargetOut,
|
||||
)
|
||||
from hotpocket_soa.dto.saves import SaveOut
|
||||
|
||||
from .base import ProxyService, SOAError
|
||||
|
||||
|
||||
class AssociationsService(ProxyService):
|
||||
class AssociationsServiceError(SOAError):
|
||||
pass
|
||||
|
||||
class AssociationNotFound(AssociationsServiceError):
|
||||
pass
|
||||
|
||||
class AssociationAccessDenied(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 create(self,
|
||||
*,
|
||||
account_uuid: uuid.UUID,
|
||||
target: SaveOut,
|
||||
pk: uuid.UUID | None = None,
|
||||
created_at: datetime.datetime | None = None,
|
||||
) -> AssociationOut:
|
||||
return AssociationOut.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'create',
|
||||
account_uuid=account_uuid,
|
||||
save_uuid=target.pk,
|
||||
pk=pk,
|
||||
created_at=created_at,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
|
||||
def get(self,
|
||||
*,
|
||||
account_uuid: uuid.UUID | None,
|
||||
pk: uuid.UUID,
|
||||
with_target: bool = False,
|
||||
allow_archived: bool = False,
|
||||
) -> AssociationOut | AssociationWithTargetOut:
|
||||
try:
|
||||
model: type[AssociationOut] = AssociationOut
|
||||
if with_target is True:
|
||||
model = AssociationWithTargetOut
|
||||
|
||||
result = model.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'get',
|
||||
pk=pk,
|
||||
with_target=with_target,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
|
||||
if allow_archived is False and result.archived_at is not None:
|
||||
raise self.AssociationNotFound(f'pk=`{pk}`')
|
||||
|
||||
if account_uuid is not None and result.account_uuid != account_uuid:
|
||||
raise self.AssociationAccessDenied(
|
||||
f'account_uuid=`{account_uuid}` pk=`{pk}`',
|
||||
)
|
||||
|
||||
return result
|
||||
except SOAError as exception:
|
||||
if isinstance(exception.__cause__, BackendAssociationsService.AssociationNotFound) is True:
|
||||
raise self.AssociationNotFound(f'pk=`{pk}`') from exception
|
||||
else:
|
||||
raise
|
||||
|
||||
def search(self,
|
||||
*,
|
||||
query: AssociationsQuery,
|
||||
limit: int,
|
||||
) -> list[AssociationWithTargetOut]:
|
||||
return [
|
||||
AssociationWithTargetOut.model_validate(row, from_attributes=True)
|
||||
for row
|
||||
in self.call(
|
||||
self.backend_associations_service,
|
||||
'search',
|
||||
query=query,
|
||||
limit=limit,
|
||||
)
|
||||
]
|
||||
|
||||
def archive(self, *, association: AssociationOut) -> bool:
|
||||
return self.call(
|
||||
self.backend_associations_service,
|
||||
'archive',
|
||||
pk=association.pk,
|
||||
)
|
||||
|
||||
def star(self, *, association: AssociationOut) -> AssociationOut:
|
||||
return AssociationOut.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'star',
|
||||
pk=association.pk,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
|
||||
def unstar(self, *, association: AssociationOut) -> AssociationOut:
|
||||
return AssociationOut.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'unstar',
|
||||
pk=association.pk,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
def delete(self, *, association: AssociationOut) -> bool:
|
||||
return self.call(
|
||||
self.backend_associations_service,
|
||||
'delete',
|
||||
pk=association.pk,
|
||||
)
|
||||
24
services/packages/soa/hotpocket_soa/services/base.py
Normal file
24
services/packages/soa/hotpocket_soa/services/base.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
class SOAError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Service:
|
||||
def wrap_exception(self, exception: Exception) -> Exception:
|
||||
return SOAError(exception.args[0])
|
||||
|
||||
|
||||
class ProxyService(Service):
|
||||
def call(self, service: typing.Any, method: str, *args, **kwargs) -> typing.Any:
|
||||
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
|
||||
40
services/packages/soa/hotpocket_soa/services/bot.py
Normal file
40
services/packages/soa/hotpocket_soa/services/bot.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from hotpocket_backend.apps.bot.services import BotService as BackendBotService
|
||||
from hotpocket_soa.dto import BotResultOut
|
||||
|
||||
from .base import ProxyService, SOAError
|
||||
|
||||
|
||||
class BotService(ProxyService):
|
||||
class BotServiceError(SOAError):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
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 is_netloc_banned(self, *, url: str) -> bool:
|
||||
return self.call(
|
||||
self.backend_associations_service,
|
||||
'is_netloc_banned',
|
||||
url=url,
|
||||
)
|
||||
|
||||
def handle(self, *, url: str) -> BotResultOut:
|
||||
return BotResultOut.model_validate(
|
||||
self.call(
|
||||
self.backend_associations_service,
|
||||
'handle',
|
||||
url=url,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from hotpocket_backend.apps.saves.services import (
|
||||
SaveProcessorService as BackendSaveProcessorService,
|
||||
)
|
||||
from hotpocket_soa.dto.celery import AsyncResultOut
|
||||
from hotpocket_soa.dto.saves import SaveOut
|
||||
|
||||
from .base import ProxyService, SOAError
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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 schedule_process_save(self, *, save: SaveOut) -> AsyncResultOut:
|
||||
result = AsyncResultOut.model_validate(
|
||||
self.call(
|
||||
self.backend_save_processor_service,
|
||||
'schedule_process_save',
|
||||
pk=save.pk,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
|
||||
LOGGER.debug('Scheduled processing task: id=`%s`', result.id)
|
||||
|
||||
return result
|
||||
59
services/packages/soa/hotpocket_soa/services/saves.py
Normal file
59
services/packages/soa/hotpocket_soa/services/saves.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from hotpocket_backend.apps.saves.services import (
|
||||
SavesService as BackendSavesService,
|
||||
)
|
||||
from hotpocket_soa.dto.saves import SaveIn, SaveOut
|
||||
|
||||
from .base import ProxyService, SOAError
|
||||
|
||||
|
||||
class SavesService(ProxyService):
|
||||
class SavesServiceError(SOAError):
|
||||
pass
|
||||
|
||||
class SaveNotFound(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)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.backend_saves_service = BackendSavesService()
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
def get(self, *, pk: uuid.UUID) -> SaveOut:
|
||||
try:
|
||||
result = SaveOut.model_validate(
|
||||
self.call(
|
||||
self.backend_saves_service,
|
||||
'get',
|
||||
pk=pk,
|
||||
),
|
||||
from_attributes=True,
|
||||
)
|
||||
|
||||
return result
|
||||
except SOAError as exception:
|
||||
if isinstance(exception.__cause__, BackendSavesService.SaveNotFound) is True:
|
||||
raise self.SaveNotFound(f'pk=`{pk}`') from exception
|
||||
else:
|
||||
raise
|
||||
15
services/packages/soa/pyproject.toml
Normal file
15
services/packages/soa/pyproject.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[tool.poetry]
|
||||
name = "hotpocket-soa"
|
||||
version = "1.0.0.dev0"
|
||||
description = "HotPocket SOA"
|
||||
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
pydantic = "2.11.7"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
Reference in New Issue
Block a user