Skip to content

Commit

Permalink
create basic influencer feed
Browse files Browse the repository at this point in the history
- add to local api
- implement routing
- test routing
- implement sequence
- test sequence
- test controller layer
- test load collection
- create influencer listing repo
- add influencer listings route
  • Loading branch information
aidangannon committed Jan 30, 2023
1 parent d852ff5 commit a53e2fa
Show file tree
Hide file tree
Showing 14 changed files with 180 additions and 22 deletions.
5 changes: 5 additions & 0 deletions local_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ def get_list_of_listings_for_brand():
return generic_handler(routeKey="GET /brands/me/listings", params={})


@app.route("/influencers/me/listings", methods=['GET'])
def get_list_of_listings_for_influencer():
return generic_handler(routeKey="GET /influencer/me/listings", params={})


@app.route("/brands/me/listings", methods=['POST'])
def create_listing_for_brand():
return generic_handler(routeKey="POST /brands/me/listings", params={})
Expand Down
16 changes: 14 additions & 2 deletions src/_types.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Protocol, Optional, Union

from src.domain.models import Brand, Influencer, Listing, User, Notification, Collaboration, AudienceAgeSplit, \
AudienceGenderSplit, BrandListing
AudienceGenderSplit, BrandListing, InfluencerListing


class AuthUserRepository(Protocol):
Expand Down Expand Up @@ -71,6 +71,12 @@ def load_for_auth_brand(self, auth_user_id: str) -> list[BrandListing]:
...


class InfluencerListingRepository(Protocol):

def load_collection(self) -> list[InfluencerListing]:
...


class CollaborationRepository(Protocol):

def write_new_for_influencer(self,
Expand Down Expand Up @@ -137,6 +143,7 @@ def save(self):
AudienceAgeRepository,
AudienceGenderRepository,
BrandListingRepository,
InfluencerListingRepository,
CollaborationRepository]

UserRepository = Union[BrandRepository, InfluencerRepository]
Expand Down Expand Up @@ -192,7 +199,12 @@ def upload(self, path: str, image_base64_encoded: str) -> str:
UserModel = Union[Brand, Influencer]

# TODO: add rest
Model = Union[UserModel, Listing, Notification, Collaboration]
Model = Union[UserModel,
Listing,
Notification,
Collaboration,
BrandListing,
InfluencerListing]


class Serializer(Protocol):
Expand Down
13 changes: 9 additions & 4 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
from src import ServiceLocator
from src._types import DataManager, BrandRepository, InfluencerRepository, ListingRepository, ImageRepository, \
Deserializer, Serializer, AuthUserRepository, Logger, NotificationRepository, AudienceAgeRepository, \
AudienceGenderRepository, BrandListingRepository, CollaborationRepository
AudienceGenderRepository, BrandListingRepository, CollaborationRepository, InfluencerListingRepository
from src.crosscutting import JsonCamelToSnakeCaseDeserializer, JsonSnakeToCamelSerializer, \
PinfluencerObjectMapper, FlexiUpdater, ConsoleLogger, DummyLogger
from src.data import SqlAlchemyDataManager
from src.data.repositories import SqlAlchemyBrandRepository, SqlAlchemyInfluencerRepository, \
SqlAlchemyListingRepository, S3ImageRepository, CognitoAuthUserRepository, CognitoAuthService, \
SqlAlchemyNotificationRepository, SqlAlchemyAudienceAgeRepository, SqlAlchemyAudienceGenderRepository, \
SqlAlchemyBrandListingRepository, SqlAlchemyCollaborationRepository
SqlAlchemyBrandListingRepository, SqlAlchemyCollaborationRepository, SqlAlchemyInfluencerListingRepository
from src.domain.validation import BrandValidator, ListingValidator, InfluencerValidator
from src.web import PinfluencerResponse, PinfluencerContext, Route
from src.web.controllers import BrandController, InfluencerController, ListingController, NotificationController, \
AudienceAgeController, AudienceGenderController, BrandListingController, CollaborationController
AudienceAgeController, AudienceGenderController, BrandListingController, CollaborationController, \
InfluencerListingController
from src.web.hooks import HooksFacade, CommonBeforeHooks, BrandAfterHooks, InfluencerAfterHooks, UserBeforeHooks, \
UserAfterHooks, InfluencerBeforeHooks, BrandBeforeHooks, ListingBeforeHooks, ListingAfterHooks, CommonAfterHooks, \
NotificationAfterHooks, NotificationBeforeHooks, AudienceAgeBeforeHooks, AudienceCommonHooks, \
Expand All @@ -37,7 +38,8 @@
GetAudienceAgeSequenceBuilder, UpdateAudienceAgeSequenceBuilder, CreateAudienceGenderSequenceBuilder, \
GetAudienceGenderSequenceBuilder, UpdateAudienceGenderSequenceBuilder, CreateInfluencerProfileSequenceBuilder, \
UpdateInfluencerProfileSequenceBuilder, GetInfluencerProfileSequenceBuilder, \
GetBrandListingsForBrandSequenceBuilder, CreateCollaborationForInfluencerSequenceBuilder
GetBrandListingsForBrandSequenceBuilder, CreateCollaborationForInfluencerSequenceBuilder, \
GetListingsForInfluencerSequenceBuilder


def lambda_handler(event, context):
Expand Down Expand Up @@ -166,6 +168,7 @@ def register_controllers(ioc):
ioc.add_singleton(AudienceAgeController)
ioc.add_singleton(AudienceGenderController)
ioc.add_singleton(BrandListingController)
ioc.add_singleton(InfluencerListingController)
ioc.add_singleton(CollaborationController)


Expand All @@ -189,6 +192,7 @@ def register_data_layer(ioc):
ioc.add_singleton(AudienceAgeRepository, SqlAlchemyAudienceAgeRepository)
ioc.add_singleton(AudienceGenderRepository, SqlAlchemyAudienceGenderRepository)
ioc.add_singleton(BrandListingRepository, SqlAlchemyBrandListingRepository)
ioc.add_singleton(InfluencerListingRepository, SqlAlchemyInfluencerListingRepository)
ioc.add_singleton(CollaborationRepository, SqlAlchemyCollaborationRepository)

# s3
Expand Down Expand Up @@ -230,4 +234,5 @@ def register_sequences(ioc: ServiceCollection):
ioc.add_singleton(UpdateInfluencerProfileSequenceBuilder)
ioc.add_singleton(GetInfluencerProfileSequenceBuilder)
ioc.add_singleton(GetBrandListingsForBrandSequenceBuilder)
ioc.add_singleton(GetListingsForInfluencerSequenceBuilder)
ioc.add_singleton(CreateCollaborationForInfluencerSequenceBuilder)
11 changes: 10 additions & 1 deletion src/data/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from src._types import DataManager, ImageRepository, Model, UserModel, Logger
from src.data.entities import create_mappings
from src.domain.models import Brand, Influencer, Listing, User, Notification, AudienceAgeSplit, AudienceAge, \
AudienceGenderSplit, AudienceGender, BrandListing, Collaboration, CollaborationStateEnum
AudienceGenderSplit, AudienceGender, BrandListing, Collaboration, CollaborationStateEnum, InfluencerListing
from src.exceptions import AlreadyExistsException, ImageException, NotFoundException

TPayload = TypeVar("TPayload")
Expand Down Expand Up @@ -250,6 +250,15 @@ def load_for_auth_brand(self, auth_user_id: str) -> list[BrandListing]:
model=BrandListing)


class SqlAlchemyInfluencerListingRepository(BaseSqlAlchemyRepository):

def __init__(self, data_manager: DataManager,
logger: Logger):
super().__init__(data_manager,
InfluencerListing,
logger=logger)


class SqlAlchemyNotificationRepository(BaseSqlAlchemyRepository):

def __init__(self, data_manager: DataManager,
Expand Down
20 changes: 17 additions & 3 deletions src/web/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from src._types import BrandRepository, UserRepository, InfluencerRepository, Repository, ListingRepository, Logger, \
Model, NotificationRepository, AudienceAgeRepository, AudienceGenderRepository, BrandListingRepository, \
CollaborationRepository
CollaborationRepository, InfluencerListingRepository
from src.crosscutting import PinfluencerObjectMapper, FlexiUpdater
from src.domain.models import Brand, Influencer, Listing, Notification, AudienceAgeSplit, AudienceGenderSplit, \
BrandListing, Collaboration
Expand All @@ -13,7 +13,7 @@
from src.web.views import BrandRequestDto, BrandResponseDto, ImageRequestDto, InfluencerRequestDto, \
InfluencerResponseDto, ListingRequestDto, ListingResponseDto, NotificationCreateRequestDto, \
NotificationResponseDto, AudienceAgeViewDto, AudienceGenderViewDto, BrandListingResponseDto, \
CollaborationResponseDto, CollaborationInfluencerCreateRequestDto
CollaborationResponseDto, CollaborationInfluencerCreateRequestDto, InfluencerListingResponseDto


class BaseController:
Expand Down Expand Up @@ -243,7 +243,6 @@ def __init__(self, repository: Repository,
response=response,
request=request,
model=None)
...

def _update_for_influencer(self,
context: PinfluencerContext,
Expand Down Expand Up @@ -421,6 +420,21 @@ def create_for_influencer(self, context: PinfluencerContext):
model=Collaboration)


class InfluencerListingController(BaseController):

def __init__(self,
listing_repo: InfluencerListingRepository,
object_mapper: PinfluencerObjectMapper,
flexi_updater: FlexiUpdater,
logger: Logger):
super().__init__(repository=listing_repo,
mapper=object_mapper,
flexi_updater=flexi_updater,
logger=logger,
response=InfluencerListingResponseDto,
request=None)


class BrandListingController(BaseOwnerController):

def __init__(self,
Expand Down
6 changes: 5 additions & 1 deletion src/web/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
GetAudienceAgeSequenceBuilder, UpdateAudienceAgeSequenceBuilder, CreateAudienceGenderSequenceBuilder, \
GetAudienceGenderSequenceBuilder, UpdateAudienceGenderSequenceBuilder, CreateInfluencerProfileSequenceBuilder, \
GetInfluencerProfileSequenceBuilder, UpdateInfluencerProfileSequenceBuilder, \
GetBrandListingsForBrandSequenceBuilder, CreateCollaborationForInfluencerSequenceBuilder
GetBrandListingsForBrandSequenceBuilder, CreateCollaborationForInfluencerSequenceBuilder, \
GetListingsForInfluencerSequenceBuilder


class Dispatcher:
Expand Down Expand Up @@ -87,6 +88,9 @@ def dispatch_route_to_ctr(self) -> dict[dict[str, Route]]:
'GET /brands/me/listings':
Route(sequence_builder=self.__service_locator.locate(GetBrandListingsForBrandSequenceBuilder)),

'GET /influencers/me/listings':
Route(sequence_builder=self.__service_locator.locate(GetListingsForInfluencerSequenceBuilder)),

'DELETE /brands/me/listings/{listing_id}':
Route(sequence_builder=self.__service_locator.locate(NotImplementedSequenceBuilder)),

Expand Down
15 changes: 14 additions & 1 deletion src/web/sequences.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from src.web import FluentSequenceBuilder, PinfluencerContext
from src.web.controllers import ListingController, InfluencerController, BrandController, NotificationController, \
AudienceAgeController, AudienceGenderController, BrandListingController, CollaborationController
AudienceAgeController, AudienceGenderController, BrandListingController, CollaborationController, \
InfluencerListingController
from src.web.hooks import CommonBeforeHooks, ListingBeforeHooks, ListingAfterHooks, UserAfterHooks, UserBeforeHooks, \
BrandBeforeHooks, BrandAfterHooks, InfluencerBeforeHooks, InfluencerAfterHooks, NotificationBeforeHooks, \
AudienceAgeBeforeHooks, CommonAfterHooks, AudienceAgeAfterHooks, AudienceGenderAfterHooks, \
Expand Down Expand Up @@ -728,6 +729,18 @@ def build(self):
._add_command(command=self.__collaboration_after_hooks.save_state)



class GetListingsForInfluencerSequenceBuilder(FluentSequenceBuilder):

def __init__(self,
influencer_listing_controller: InfluencerListingController):
super().__init__()
self.__influencer_listing_controller = influencer_listing_controller

def build(self):
self._add_command(command=self.__influencer_listing_controller.get_all)


class NotImplementedSequenceBuilder(FluentSequenceBuilder):

def __init__(self):
Expand Down
5 changes: 5 additions & 0 deletions src/web/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ class BrandListingResponseDto(ListingResponseDto):
applied_collaborations: list[CollaborationResponseDto] = field(default_factory=list)


@dataclass(unsafe_hash=True)
class InfluencerListingResponseDto(ListingResponseDto):
brand: BrandResponseDto = None


@dataclass(unsafe_hash=True)
class NotificationResponseDto(BaseResponseDto):
receiver_auth_user_id: str = None
Expand Down
8 changes: 8 additions & 0 deletions template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,14 @@ Resources:
Path: /brands/me/listings
Method: get
ApiId: !Ref PinfluencerHttpApi
GetAllMyInfluencerListings:
Type: HttpApi
Properties:
Auth:
Authorizer: UserAuth
Path: /influencers/me/listings
Method: get
ApiId: !Ref PinfluencerHttpApi
UpdateMyListingById:
Type: HttpApi
Properties:
Expand Down
21 changes: 20 additions & 1 deletion tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
GetAudienceAgeSequenceBuilder, UpdateAudienceAgeSequenceBuilder, CreateAudienceGenderSequenceBuilder, \
GetAudienceGenderSequenceBuilder, UpdateAudienceGenderSequenceBuilder, CreateInfluencerProfileSequenceBuilder, \
UpdateInfluencerProfileSequenceBuilder, GetInfluencerProfileSequenceBuilder, \
GetBrandListingsForBrandSequenceBuilder, CreateCollaborationForInfluencerSequenceBuilder
GetBrandListingsForBrandSequenceBuilder, CreateCollaborationForInfluencerSequenceBuilder, \
GetListingsForInfluencerSequenceBuilder
from tests import get_as_json


Expand Down Expand Up @@ -324,6 +325,24 @@ def test_get_auth_brand_listings(self):
.assert_called_once_with(context=Any(),
sequence=self.__ioc.resolve(GetBrandListingsForBrandSequenceBuilder))

def test_get_auth_influencer_listings(self):
# arrange
self.__mock_middleware_pipeline.execute_middleware = MagicMock()

# act
bootstrap(event={"routeKey": "GET /influencers/me/listings"},
context={},
middleware=self.__mock_middleware_pipeline,
ioc=self.__ioc,
data_manager=Mock(),
cognito_auth_service=Mock())

# assert
self.__mock_middleware_pipeline \
.execute_middleware \
.assert_called_once_with(context=Any(),
sequence=self.__ioc.resolve(GetListingsForInfluencerSequenceBuilder))

def test_update_brand_auth_listing_by_id(self):
# arrange
self.__mock_middleware_pipeline.execute_middleware = MagicMock()
Expand Down
32 changes: 28 additions & 4 deletions tests/test_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@
from ddt import ddt, data

from src._types import BrandRepository, InfluencerRepository, ListingRepository, NotificationRepository, \
AudienceAgeRepository, AudienceGenderRepository, BrandListingRepository, CollaborationRepository
AudienceAgeRepository, AudienceGenderRepository, BrandListingRepository, CollaborationRepository, \
InfluencerListingRepository
from src.app import logger_factory
from src.crosscutting import AutoFixture, FlexiUpdater
from src.domain.models import Influencer, Listing, Brand, Notification, AudienceAgeSplit, AudienceGenderSplit, \
AudienceGender, Collaboration
AudienceGender, Collaboration, InfluencerListing
from src.exceptions import AlreadyExistsException, NotFoundException
from src.web import PinfluencerContext, PinfluencerResponse
from src.web.controllers import BrandController, InfluencerController, ListingController, NotificationController, \
AudienceAgeController, AudienceGenderController, BrandListingController, CollaborationController
AudienceAgeController, AudienceGenderController, BrandListingController, CollaborationController, \
InfluencerListingController
from src.web.error_capsules import AudienceDataNotFoundErrorCapsule
from src.web.views import BrandRequestDto, BrandResponseDto, ImageRequestDto, InfluencerRequestDto, \
InfluencerResponseDto, ListingRequestDto, ListingResponseDto, NotificationCreateRequestDto, \
NotificationResponseDto, AudienceAgeViewDto, AudienceGenderViewDto, BrandListingResponseDto, \
CollaborationInfluencerCreateRequestDto, CollaborationResponseDto
CollaborationInfluencerCreateRequestDto, CollaborationResponseDto, InfluencerListingResponseDto
from tests import test_mapper


Expand Down Expand Up @@ -1032,3 +1034,25 @@ def test_get_for_brand(self):
# assert
with self.subTest(msg="repo was called"):
self.__sut._get_for_auth_user.assert_called_once_with(context=context, response=BrandListingResponseDto)


class TestInfluencerListingController(TestCase):

def setUp(self):
self.__repository: InfluencerListingRepository = Mock()
self.__sut = InfluencerListingController(listing_repo=self.__repository,
object_mapper=test_mapper(),
flexi_updater=FlexiUpdater(mapper=test_mapper()),
logger=Mock())

def test_get_all(self):
# arrange
context = PinfluencerContext()
influencer_listings = AutoFixture().create_many(dto=InfluencerListing, ammount=5)
self.__sut._get_all = MagicMock(return_value=influencer_listings)

# act
self.__sut.get_all(context=context)

self.__sut._get_all.assert_called_once_with(context=context,
response=InfluencerListingResponseDto)
1 change: 0 additions & 1 deletion tests/test_data_mapping.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from operator import itemgetter
from unittest import TestCase

from src.app import logger_factory
Expand Down
24 changes: 22 additions & 2 deletions tests/test_repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
from src.crosscutting import AutoFixture
from src.data.repositories import SqlAlchemyBrandRepository, SqlAlchemyInfluencerRepository, CognitoAuthUserRepository, \
CognitoAuthService, SqlAlchemyListingRepository, SqlAlchemyNotificationRepository, SqlAlchemyAudienceAgeRepository, \
SqlAlchemyAudienceGenderRepository, SqlAlchemyBrandListingRepository, SqlAlchemyCollaborationRepository
SqlAlchemyAudienceGenderRepository, SqlAlchemyBrandListingRepository, SqlAlchemyCollaborationRepository, \
SqlAlchemyInfluencerListingRepository
from src.domain.models import Brand, Influencer, User, Listing, Notification, AudienceAgeSplit, AudienceAge, \
AudienceGenderSplit, AudienceGender, BrandListing, Collaboration, CollaborationStateEnum
AudienceGenderSplit, AudienceGender, BrandListing, Collaboration, CollaborationStateEnum, InfluencerListing
from src.exceptions import AlreadyExistsException, NotFoundException
from tests import InMemorySqliteDataManager

Expand Down Expand Up @@ -332,6 +333,25 @@ def test_get_user_by_id(self):
assert actual_brand.email == expected_brand.email


class TestInfluencerListingRepository(TestCase):

def setUp(self) -> None:
self.__data_manager = InMemorySqliteDataManager()
self.__sut = SqlAlchemyInfluencerListingRepository(data_manager=self.__data_manager,
logger=Mock())

def test_load_collection(self):
# arrange
influencer_listings = AutoFixture().create_many(dto=InfluencerListing, ammount=5, list_limit=5)
self.__data_manager.create_fake_data(influencer_listings)

# act
returned_listings = self.__sut.load_collection()

# assert
self.assertEqual(returned_listings, influencer_listings)


class TestBrandListingRepository(TestCase):

def setUp(self) -> None:
Expand Down
Loading

0 comments on commit a53e2fa

Please sign in to comment.