Skip to content

Commit 0b8514d

Browse files
committed
add command registry, module.execute_command method, unit of work, tests
1 parent d05566a commit 0b8514d

13 files changed

Lines changed: 241 additions & 239 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ src-gen/
66
htmlcov/
77
logs.json
88
tmp/
9+
.idea/
10+
poetry.lock

.idea/dataSources.xml

Lines changed: 0 additions & 12 deletions
This file was deleted.

.idea/misc.xml

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/cli/__main__.py

Lines changed: 23 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,46 @@
1-
# configure logger prior to first usage
2-
import uuid
3-
4-
from sqlalchemy.orm import Session
5-
61
from config.container import Container
7-
from modules.catalog.domain.entities import Listing, Money
8-
from modules.catalog.infrastructure.listing_repository import (
9-
Base,
10-
PostgresJsonListingRepository,
2+
from modules.catalog.application.command.create_listing_draft import (
3+
CreateListingDraftCommand,
114
)
5+
from modules.catalog.domain.entities import Money
6+
from modules.catalog.infrastructure.listing_repository import Base
127
from seedwork.infrastructure.logging import LoggerFactory, logger
13-
from seedwork.infrastructure.request_context import request_context
148

159
# a sample command line script to print all listings
1610
# run with "cd src && python -m cli"
1711

18-
12+
# configure logger prior to first usage
1913
LoggerFactory.configure(logger_name="cli")
2014

2115

2216
container = Container()
2317
container.config.from_dict(
2418
dict(
25-
DATABASE_URL="sqlite+pysqlite:///:memory:",
19+
# DATABASE_URL="sqlite+pysqlite:///:memory:",
20+
DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres",
2621
DEBUG=True,
2722
)
2823
)
2924

3025
engine = container.engine()
3126
Base.metadata.create_all(engine)
3227

33-
listing_id = uuid.uuid4()
34-
35-
with Session(engine) as session:
36-
request_context.correlation_id.set(uuid.uuid4())
37-
logger.info("Session 1")
38-
print(engine, session)
39-
repo = PostgresJsonListingRepository(session)
40-
listing = Listing(
41-
id=listing_id,
42-
seller_id=uuid.uuid4(),
43-
title="Foo",
44-
description="",
45-
ask_price=Money(1),
46-
)
47-
repo.add(listing)
28+
catalog_module = container.catalog_module()
4829

30+
with catalog_module.unit_of_work() as uow:
31+
logger.info(f"executing unit of work")
32+
count = uow.listing_repository.count()
33+
logger.info(f"{count} listing in the repository")
4934

50-
with Session(engine) as session:
51-
request_context.correlation_id.set(uuid.uuid4())
52-
logger.info("Session 2")
53-
repo = PostgresJsonListingRepository(session)
54-
repo.get_by_id(listing_id)
55-
56-
57-
# # configure catalog module
58-
#
59-
#
60-
# # instantiate catalog module
61-
# catalog_module = container.catalog_module()
62-
#
63-
# logger.info("Application configured")
64-
#
65-
# # let's generate a fake seller id for now
66-
# seller_id = SellerRepository.next_id()
67-
#
68-
#
69-
# ###############################
70-
# from modules.catalog.infrastructure.listing_repository import PostgresJsonListingRepository
71-
#
72-
#
73-
# session = ...
74-
# repository = PostgresJsonListingRepository(sqla_session=session)
75-
76-
77-
# from contextlib import contextmanager
78-
# from modules.catalog.module import CatalogModule
79-
#
80-
# @contextmanager
81-
# def business_tansaction():
82-
# try:
83-
# unit_of_work = ...
84-
#
85-
# yield CatalogModule(unit_of_work, listing_repository=container.listing_repository())
86-
# finally:
87-
# ...
88-
#
89-
#
90-
#
91-
#
92-
# with BusinessTransactionOn(CatalogModule) as catalog_module:
93-
# catalog_module.foo_repo()
94-
#
95-
#
96-
#
97-
# with catalog_module as transaction:
98-
# print(transaction)
99-
35+
with catalog_module.unit_of_work():
36+
logger.info(f"adding new draft")
37+
catalog_module.execute_command(
38+
CreateListingDraftCommand(
39+
title="First listing", description=".", ask_price=Money(100), seller_id=None
40+
)
41+
)
10042

101-
#
102-
# # interact with a catalog module by issuing queries and commands
103-
# # use request context if you want to logically separate queries/commands
104-
# # from each other in the logs
105-
# with request_context:
106-
# command = CreateListingDraftCommand(
107-
# title="Foo", description="Bar", ask_price=1, seller_id=seller_id
108-
# )
109-
# result = catalog_module.execute_command(command)
110-
# print(result)
111-
# if result.is_ok():
112-
# logger.info("Draft added")
113-
# else:
114-
# logger.error(result.get_errors())
115-
#
116-
# with request_context:
117-
# query_result = catalog_module.execute_query(GetAllListings())
118-
# logger.info(f"All listings: {query_result.result}")
119-
#
120-
# with request_context:
121-
# query_result = catalog_module.execute_query(
122-
# GetListingsOfSeller(seller_id=seller_id)
123-
# )
124-
# logger.info(f"Listings of seller {seller_id}: {query_result.result}")
43+
with catalog_module.unit_of_work() as uow:
44+
logger.info(f"executing unit of work")
45+
count = uow.listing_repository.count()
46+
logger.info(f"{count} listing in the repository")

src/config/container.py

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,7 @@
22
from dependency_injector.wiring import inject # noqa
33
from sqlalchemy import create_engine
44

5-
from modules.catalog.infrastructure.listing_repository import (
6-
PostgresJsonListingRepository,
7-
)
8-
from modules.catalog.module import CatalogModule
9-
from modules.iam.application.services import AuthenticationService
10-
from modules.iam.infrastructure.user_repository import PostgresJsonUserRepository
11-
from modules.iam.module import IdentityAndAccessModule
12-
from seedwork.infrastructure.request_context import RequestContext
5+
from modules.catalog import CatalogModule
136

147

158
def _default(val):
@@ -62,32 +55,8 @@ class Container(containers.DeclarativeContainer):
6255

6356
config = providers.Configuration()
6457
engine = providers.Singleton(create_engine_once, config)
65-
dummy_service = providers.Factory(DummyService, config)
66-
dummy_singleton = providers.Singleton(DummyService, config)
6758

68-
request_context: RequestContext = providers.Factory(
69-
create_request_context, engine=engine
70-
)
71-
72-
correlation_id = providers.Factory(
73-
lambda request_context: request_context.correlation_id.get(), request_context
74-
)
75-
76-
# catalog module and it's dependencies
77-
listing_repository = providers.Factory(
78-
PostgresJsonListingRepository, db_session=request_context.provided.db_session
79-
)
8059
catalog_module = providers.Factory(
81-
CatalogModule, listing_repository=listing_repository
82-
)
83-
84-
# iam module and it's dependencies
85-
user_repository = providers.Factory(
86-
PostgresJsonUserRepository, db_session=request_context.provided.db_session
87-
)
88-
authentication_service = providers.Factory(
89-
AuthenticationService, user_repository=user_repository
90-
)
91-
iam_module = providers.Factory(
92-
IdentityAndAccessModule, authentication_service=authentication_service
60+
CatalogModule,
61+
engine=engine,
9362
)

src/modules/bidding/application/query/get_pastdue_listings.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
from dataclasses import dataclass, field
12
from datetime import datetime
23

34
from modules.bidding.domain.repositories import ListingRepository
45
from seedwork.application.decorators import query_handler
5-
from seedwork.application.queries import Field, Query
6+
from seedwork.application.queries import Query
67
from seedwork.application.query_handlers import QueryResult
78

89

10+
@dataclass
911
class GetPastdueListingsQuery(Query):
10-
now: datetime = Field(default_factory=datetime.utcnow)
12+
now: datetime = field(default_factory=datetime.utcnow)
1113

1214

1315
@query_handler

src/modules/bidding/test_module.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
from modules.bidding.application.query.get_pastdue_listings import (
2-
GetPastdueListingsQuery,
3-
)
4-
from modules.bidding.module import BiddingModule
5-
from seedwork.infrastructure.repository import InMemoryRepository
1+
# from modules.bidding.application.query.get_pastdue_listings import (
2+
# GetPastdueListingsQuery,
3+
# )
4+
# from modules.bidding.module import BiddingModule
5+
# from seedwork.infrastructure.repository import InMemoryRepository
6+
#
7+
#
8+
# class DummyEventPublisher:
9+
# ...
10+
#
611

7-
8-
class DummyEventPublisher:
9-
...
10-
11-
12-
def test_module():
13-
module = BiddingModule(
14-
event_publisher=DummyEventPublisher(), listing_repository=InMemoryRepository()
15-
)
16-
17-
query = GetPastdueListingsQuery()
18-
result = module.execute_query(query)
19-
20-
assert result.is_ok()
21-
assert result.get_result() == []
12+
# def test_module():
13+
# module = BiddingModule(
14+
# event_publisher=DummyEventPublisher(), listing_repository=InMemoryRepository()
15+
# )
16+
#
17+
# query = GetPastdueListingsQuery()
18+
# result = module.execute_query(query)
19+
#
20+
# assert result.is_ok()
21+
# assert result.get_result() == []

src/modules/catalog/__init__.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import uuid
2+
from contextlib import contextmanager
3+
from contextvars import ContextVar
4+
from dataclasses import dataclass
5+
6+
from sqlalchemy.orm import Session
7+
8+
from modules.catalog.application.command import (
9+
CreateListingDraftCommand,
10+
PublishListingCommand,
11+
UpdateListingDraftCommand,
12+
)
13+
from modules.catalog.application.command.create_listing_draft import (
14+
create_listing_draft,
15+
)
16+
from modules.catalog.application.query import GetAllListings
17+
from modules.catalog.domain.repositories import ListingRepository
18+
from modules.catalog.infrastructure.listing_repository import (
19+
PostgresJsonListingRepository,
20+
)
21+
from seedwork.application.decorators import registry
22+
from seedwork.infrastructure.request_context import request_context
23+
24+
25+
def handles_command(command_class):
26+
...
27+
28+
29+
def handles_query(query_class):
30+
...
31+
32+
33+
class implemented_as:
34+
def __init__(self, repo_class):
35+
self.repo_class = repo_class
36+
37+
def __get__(self, obj, obj_type=None):
38+
raise NotImplementedError()
39+
40+
41+
def get_arg(name, kwargs1, kwargs2):
42+
return kwargs1.get(name, None) or kwargs2.get(name)
43+
44+
45+
@dataclass
46+
class UnitOfWork:
47+
db_session: Session
48+
correlation_id: uuid.UUID
49+
listing_repository: ListingRepository
50+
51+
52+
class CatalogModule:
53+
handles_command(CreateListingDraftCommand)
54+
handles_query(GetAllListings)
55+
56+
def __init__(self, **kwargs):
57+
self._uow: ContextVar[UnitOfWork] = ContextVar("_uow", default=None)
58+
self.extra_kwargs = kwargs
59+
60+
@contextmanager
61+
def unit_of_work(self, **kwargs):
62+
engine = get_arg("engine", kwargs, self.extra_kwargs)
63+
correlation_id = uuid.uuid4()
64+
db_session = None
65+
with Session(engine) as db_session:
66+
uow = UnitOfWork(
67+
correlation_id=correlation_id,
68+
db_session=db_session,
69+
listing_repository=PostgresJsonListingRepository(db_session=db_session),
70+
)
71+
# begin unit of work
72+
self._uow.set(uow)
73+
self.begin_unit_of_work(uow)
74+
yield uow
75+
self.end_unit_of_work(uow)
76+
# end unit of work
77+
self._uow.set(None)
78+
79+
def begin_unit_of_work(self, uow: UnitOfWork):
80+
request_context.correlation_id.set(uow.correlation_id)
81+
82+
def end_unit_of_work(self, uow):
83+
uow.listing_repository.persist_all()
84+
uow.db_session.commit()
85+
86+
def configure(self, **kwargs):
87+
self.extra_kwargs = kwargs
88+
89+
def execute_command(self, command):
90+
command_class = type(command)
91+
handler = registry.get_command_handler_for(command_class)
92+
kwargs = registry.get_command_handler_parameters_for(command_class)
93+
94+
for param_name, param_type in kwargs.items():
95+
for attr in self.uow.__dict__.values():
96+
if isinstance(attr, param_type):
97+
kwargs[param_name] = attr
98+
99+
return handler(command=command, **kwargs)
100+
101+
@property
102+
def uow(self) -> UnitOfWork:
103+
uow = self._uow.get()
104+
print(self._uow, self._uow.get())
105+
assert uow, "Unit of work not set, use context manager"
106+
return uow
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .create_listing_draft import CreateListingDraftCommand
2+
from .publish_listing import PublishListingCommand
3+
from .update_listing_draft import UpdateListingDraftCommand
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .get_all_listings import GetAllListings
2+
from .get_listing_details import GetListingDetails
3+
from .get_listings_of_seller import GetListingsOfSeller

0 commit comments

Comments
 (0)