Package: pydoca
pydocaDomain-Oriented Clean Architecture python library.A marriage of Uncle Bob's Clean Architecture and Eric Evan's Domain-Driven Design, for Python developers.QuickstartInstall usingpip install pydocaHow to useDisclaimer: This is a very trivial example, a more complex one can be found in the integration tests.Create your domain first.# app/domain/car.pyfromtypingimportLiteralimportpydocaclassTireChanged(pydoca.Event):position:strreference:strclassTire(pydoca.Entity):reference:strposition:Literal["front-right","front-left","back-right","back-left"]wear:float=1.0def_id(self)->str:returnf"{self.reference}-{self.position}".lower()classCar(pydoca.AggregateRoot):vin:strtires:list[Tire]=[]def_id(self)->str:returnself.vin.lower()defchange_tire(self,new_tire:Tire)->None:tire_to_change_idx=next(idxforidx,tireinenumerate(self.tires)iftire.position==new_tire.position)self.tires.pop(tire_to_change_idx)self.tires.append(new_tire)self.add_event(TireChanged(position=new_tire.position,reference=new_tire.reference))Then your use case. Your domain and your use cases should not depend on external dependencies.-> dependency directionactors|adapters -> application -> domain# app/application/change_tire.pyimportabcimportpydocafromapp.domain.carimportCar,TireclassCarRepo(pydoca.Repository):@abc.abstractmethoddefget_by_id(self,car_id:str)->Car:"""Gets a car or raises EntityNotFoundError."""@abc.abstractmethoddefsave(self,car:Car)->Car:"""Saves a car."""classChangeTireCmd(pydoca.Command):car_id:strreference:strposition:strclassChangeTire(pydoca.UseCase):classUnitOfWork:car_repo:CarRepodefexec(self,cmd:ChangeTireCmd)->Car:withself.uowasuow:# The UOW will automatically push your aggregates events to the event bus.car:Car=uow.car_repo.get_by_id(cmd.car_id)car.change_tire(Tire(position=cmd.position,reference=cmd.reference))returncarNow you need an actor, your application entry point calling the use case.
Let's use FastAPI for example.# app/actors/api.pyimportfastapifromapp.application.change_tireimportChangeTire,ChangeTireCmdfromapp.domain.carimportCarapp=fastapi.FastAPI()@app.put("/car/{car_id}/change_tire")defchange_tire(payload:ChangeTireCmd)->Car:returnChangeTire().exec(payload)Last step is to implement the car repository in adapters and configure the project.# app/adapters/inmemory_car_repo.pyimportcollections.abcfromtypingimportIterator,Selfimportpydocafromapp.application.change_tireimportCarRepositoryfromapp.domain.carimportCar,Tiredb={"fake_car":Car(vin="fake_car",tires=[Tire(reference="michelinf",position="front-right"),Tire(reference="michelinb",position="back-right"),Tire(reference="michelinf",position="front-left"),Tire(reference="michelinb",position="back-left",wear=0.1),])}classInMemorySession(pydoca.Session,collections.abc.MutableMapping):def__init__(self):self.store:dict[str,Car]=dbdef__setitem__(self,key:str,val:Car)->None:self.store[key]=valdef__delitem__(self,key:str)->None:delself.store[key]def__getitem__(self,key:str)->Car:returnself.store[key]def__len__(self)->int:returnlen(self.store)def__iter__(self)->Iterator[str]:returniter(self.store)@classmethoddefstart(cls)->Self:returncls()@classmethoddefurl(cls)->str:return"//memory"defcommit(self)->None:print("Commit")defrollback(self)->None:print("Rollback")classInMemoryCarRepo(CarRepository):sessionT=InMemorySessiondefget_by_id(self,car_id:str)->Car:ifcar:=self.session.get(car_id):returncarelse:raisepydoca.EntityNotFoundError(class_id=(Car,car_id))defsave(self,car:Car)->Car:self.session[car.id]=carreturncar# app/local_configuration.pyimportpydocafromapp.adapters.inmemory_car_repoimportInMemoryCarRepoclassConfiguration(pydoca.AdaptersConfig):CarRepository=InMemoryCarRepo# app/main.pyimportpydocaimportuvicornfromapp.local_configurationimportConfigurationif__name__=="__main__":pydoca.bootstrap(adapters_config=Configuration)uvicorn.run("app.actors.api:app")Then you can execute the main file and visithttp://localhost:8080/docsand use the swagger to change the back-left tire of fake_car:)pipinstallfastapiuvicornpydoca
pythonapp/main.pyDevelopmentTODO (In order of importance):publish to pypi(Alpha version at this point)Fix mypy for pytest (remove pre-config tests exclusion)100% tests coverageFix type hints and Pycharm autocompletion featuresRe-work events, maybe context python bus not the good solutionmkdocsAllow to hide some Command attributes in the model and be able to set them laterbinary to analyze code like mypy and give errors/warnings/feedbacksAdd modules for easy integration with fastapi, cli tools, aws lambda etc.UnitOfWork manages multiple sessions?Improve tests, integration with real DBs, multiple actors etc.(Beta version at this point)