Package: galo_ioc
Galo-IOC🇷🇺Russian versionDescriptionA plugin system is well suited for creating flexible and extensible applications. In such a system, plugins are responsible for creating and binding application objects to each other (for example, various implementations of services or repositories). To do this, you need to have a storage of all objects. TheService Locatorpattern copes with this role perfectly, and the Galo-IOC project is an easy-to-use and lightweight implementation of it.The plugin system together with Galo-IOC will help if necessary:conveniently enable/disable parts of the application functionality;install the application to several clients, some of whom must have some functionality individually;extend the application by installing third-party packages.Main featuresInstandard implementationof the Service Locator pattern, a single instance of each class is stored. In this library, instead of an instance, a factory is stored, which allows you to flexibly manage the creation of objects.To get objects of the same type, you can register several factory implementations and choose between them.Support for passing parameters when calling the factory.Support for static code analysis. Thanks to this, the IDE suggests the names and types of parameters, as well as the type of the returned result when calling factories, which greatly simplifies writing code and avoids stupid mistakes.Support for decorators for factories, with which you can influence the creation of objects. For example, add logging for all created objects or add caching for objects of some type.It is worth noting that Service Locatoris an antipattern. Its disadvantages include hiding dependencies. The examples below show a way to use this library, reducing the disadvantages of the Service Locator pattern to a minimum: Service Locator is used only in plugins, but not in the application classes. In the application classes, dependencies are explicitly specified in the constructors.Usage examplesTo demonstrate the capabilities of the library, consider the following example. An IT company is developing a product that allows their customers — other companies — to congratulate employees on birthday via a messenger. Among the customers there are companies from different countries, and the list of messengers used includes WhatsApp, Telegram and internal corporate messengers of companies.Plugin systemThis library works well with any plugin system, in which plugins allow you to create and bind application objects (services, repositories) with each other. This library does not provide an implementation of the plugin system, because it is not its responsibility. To use this library, you will have to take a ready-made implementation of the plugin system or implement it yourself. For these examples, we implement the plugin system ourselves.This implementation of the plugin system will be very simple, but at the same time functional enough to demonstrate all the features of the Galo-IOC library. In this plugin system, the configuration file will contain the names of the modules. Each such module will contain aloadfunction, which will be responsible for creating and registering application objects in the Service Locator. When the application starts, these modules will be imported, and then theloadfunction will be called for each of them.An application for congratulating employeesProject structureThe project will have the following structure:.
├── module_names.txt
├── setup.py
└── src
   └── congratulations_app
      ├── __init__.py
      ├── __main__.py
      ├── congratulations_services
      │  ├── __init__.py
      │  ├── english.py
      │  └── russian.py
      └── messengers
         ├── __init__.py
         ├── telegram.py
         └── whatsapp.pyConfiguration fileFilemodule_names.txtis the configuration file that lists the modules. These modules will be imported and theloadfunction will be called for each of them. Example of file contentsmodule_names.txt:congratulations_app.messengers.telegram
congratulations_app.congratulations_services.russianAs you can see, Telegram is used as a messenger. If there is a need to replace the Telegram messenger with WhatsApp, for example, when installing an application to another customer with such a requirement, it will be enough to replace the linecongratulations_app.messengers.telegramwithcongratulations_app.messengers.whatsappin the configuration file of the new customer. In this way, you can replace any application object with any other without having to modify the code.Implementation of messengersThe filesrc/congratulations_app/messengers/__init__.pycontains the messenger interface —Messengerand the messenger factory interface —MessengerFactory. The factory interface is needed to specify the contract that other modules will use to get this object.# src/congratulations_app/messengers/__init__.py__all__=["Messenger","MessengerFactory",]classMessenger:defsend_message(self,name:str,message:str)->None:raiseNotImplementedError()classMessengerFactory:def__call__(self)->Messenger:raiseNotImplementedError()Let's consider one of the implementations of the messenger — Telegram, which is contained in the modulesrc/congratulations_app/messengers/telegram.py. This module contains the implementation of theMessengerinterface —TelegramMessengerand theloadfunction. This function will be called when the application is initialized if this module is specified in the configuration filemodule_names.txt. The function creates an instance of theTelegramMessengerclass and the factoryTelegramMessengerFactorythat returns the messenger instance. This factory is then registered in the Service Locator using theadd_factoryfunction from the Galo-IOC library. After that, using this factory, it will be possible to get an instance of theMessengerclass in another module. The module contained the WhatsApp messenger is implemented in a similar way —src/congratulations_app/messengers/whatsapp.py.# src/congratulations_app/messengers/telegram.pyfromgalo_iocimportadd_factoryfromcongratulations_app.messengersimportMessenger,MessengerFactory__all__=["TelegramMessenger","load",]classTelegramMessenger(Messenger):defsend_message(self,name:str,message:str)->None:print(f"Message{message!r}sent to{name!r}via Telegram.")defload()->None:classTelegramMessengerFactory(MessengerFactory):def__call__(self)->Messenger:returnmessengermessenger=TelegramMessenger()add_factory(MessengerFactory,TelegramMessengerFactory())Implementation of congratulations servicesNow let's move on to one of the implementations of the congratulations service, which is contained in the modulesrc/congratulations_app/congratulation_services/russian.pyTheloadfunction in this module is responsible for creating an object of theRussianCongratulationsServicetype and registering its factory in the Services Locator. To get themessengerdependency, theget_factoryfunction is used. It allows you to access theMessengerFactory, which is currently registered in the Services Locator. It can beTelegramMessengerFactory,WhatsAppMessengerFactoryor any other. Then an instance of theMessengerclass is gotten by calling this factory. After that, it is passed to the constructor of theRussianCongratulationsServiceclass to create it.# src/congratulations_app/congratulation_services/russian.pyfromgalo_iocimportadd_factory,get_factoryfromcongratulations_app.messengersimportMessenger,MessengerFactoryfromcongratulations_app.congratulations_servicesimportCongratulationsService,CongratulationsServiceFactory__all__=["RussianCongratulationsService","load",]classRussianCongratulationsService(CongratulationsService):def__init__(self,messenger:Messenger)->None:self.__messenger=messengerdefhappy_birthday(self,name:str)->None:self.__messenger.send_message(name,f"С днем рождения,{name}!")defload()->None:classRussianCongratulationsServiceFactory(CongratulationsServiceFactory):def__call__(self)->CongratulationsService:returnservicemessenger_factory=get_factory(MessengerFactory)messenger=messenger_factory()service=RussianCongratulationsService(messenger)add_factory(CongratulationsServiceFactory,RussianCongratulationsServiceFactory())Thanks to the use of factory interfaces, static code analysis and autocompletion are supported.Implementation of the application startup functionIn the application startup function, the configuration file is read and modules are loaded. Theget_factoryfunction (similar toset_factory) accesses the container of factories in the current context. To add a container of factories to the current context, use the expressionwith FactoryContainerImpl():.fromgalo_iocimportFactoryContainerImpl,get_factoryfromcongratulations_app.startup_utilsimportget_module_names_path,read_module_names,load_pluginsfromcongratulations_app.congratulations_servicesimportCongratulationsServiceFactorydefmain()->None:module_names_path=get_module_names_path()module_names=read_module_names(module_names_path)withFactoryContainerImpl():load_plugins(module_names)congratulations_service_factory=get_factory(CongratulationsServiceFactory)congratulations_service=congratulations_service_factory()congratulations_service.happy_birthday("Maria")if__name__=="__main__":main()With the contents of the filemodule_names.txt:congratulations_app.messengers.telegram
congratulations_app.congratulations_services.russianThe output will be:Message 'С днем рождения, Maria!' sent to 'Maria' via Telegram.But if you change the contents of the filemodule_names.txton:congratulations_app.messengers.whatsapp
congratulations_app.congratulations_services.englishYou get the output:Message 'Happy birthday, Maria!' sent to 'Maria' via WhatsApp.Implementation of third-party pluginsNow let's look at the integration of third-party plugins into the application. For example, a new customer wants to use an application to congratulate employees on birthday, but it does not want to use any of the already implemented messengers, but instead wants to use its internal corporate messenger. At the same time, this customer is against including the implementation of its corporate messenger in the code base of the application. Even this case is not a problem for the Galo-IOC library together with the plugin system. To solve this problem, you need to create a separate project.Project structureThe project structure will look like this:.
├── setup.py
└── src
   └── secret_corporation_plugin
      ├── __init__.py
      └── messengers
         ├── __init__.py
         └── secret_corporation.pyImplementation of an internal corporate messengerConsider the implementation of the modulesrc/secret_corporation_plugin/messengers/secret_corporation.py. As you can see, it does not fundamentally differ from the implementation of the other two messengers: Telegram and WhatsApp, included in the code base of the application.# src/secret_corporation_plugin/messengers/secret_corporation.pyfromgalo_iocimportadd_factoryfromcongratulations_app.messengersimportMessenger,MessengerFactory__all__=["SecretCorporationMessenger","load",]classSecretCorporationMessenger(Messenger):defsend_message(self,name:str,message:str)->None:print(f"Message{message!r}sent to{name!r}via Secret Corporation Messenger.")defload()->None:classSecretCorporationMessengerFactory(MessengerFactory):def__call__(self)->Messenger:returnmessengermessenger=SecretCorporationMessenger()add_factory(MessengerFactory,SecretCorporationMessengerFactory())Integration of a third-party plugin into the applicationTo use the implementation of the internal corporate messenger in the application instead ofTelegramorWhatsApp, you need to install thesecret_corporation_pluginpackage using the commandpython setup.py install .in the root directory of the project with this messenger. Further, in the configuration filemodule_names.txtspecifysecret_corporation_plugin.messengers.secret_corporationas a module with a messenger. As a result, the contents of the filemodule_names.txtmay look like this:secret_corporation_plugin.messengers.secret_corporation
congratulations_app.congratulations_services.russianAnd when running an application with such configuration file content we get the following output:Message 'С днем рождения, Maria!' sent to 'Maria' via Secret Corporation Messenger.As you can see,SecretCorporationMessengeris used as a messenger. To achieve this, it was not necessary to change the application code, but it was enough just to add another implementation of the messenger in another project and change the configuration file.The full version of the example can be found atlink.More examplesMore examples can be found atlink.loggerscontains an example of a factory with input arguments.congratulations_service_auditcontains an example of using a decorator. The decorator is used for logging of input arguments for theCongratulationsService.fastapi_integrationcontains an example of integration with the FastAPI web framework. This example implements:two error handlers: text and json;two user repositories: Memory and PostgreSQL;two authentication methods: Basic authentication and OAuth 2;and other functionality.You can select the used implementations of the error handler, the user repository, and the authentication method in the configuration filemodule_names.txt.