Package: gasofo
Gasofo (framework for Qwil's hexagonal code architecture)Gasofo is Qwil's take on a implementing a Hexagonal code architecture
(1,2,3,4).See./examplefor an example of how gasofo can be used to build up an app.Installing Gasofopip install gasofoDefining ServicesServices should be stateless and should only access resources other services via its "Needs" ports. Functionality
provided by a service are exposed via "Provides" ports.A Service can be defined as such:fromgasofoimportService,Needs,providesclassMyService(Service):deps=Needs(['some_data','another_service'])# ports this service 'Needs'@providesdefmy_feature(self,x):data=self.deps.some_data()# needs are accessed via self.deps.<port_name>more_data=self.deps.another_service(value=x)returndata+more_dataHere we defined the serviceMyServicewith a couple of Needs ports and a single Provides port named
"my_feature".Methods on instances of this class can be called just like a regular class, but only the ones tagged
with@providesare discoverable as a port.Dependencies of the service are access through the Needs port viaself.deps.PORT_NAME(...). These ports will be
injected with actual provider functions when the application is wired up. Calling a port before it is connected to a
provider will raise aDisconnectedPortexception.The ports of a service can be queried by callingget_needs()andget_provides()on the service class or instance.
This helps with the visualisation and auto-wiring of Services to form business domains and complete applications.Validations on declarationExceptions will be raised during class construction (which usually means when you import the module) if the following
validations fail:Constructors (__init__) are not allowed since services are meant to be stateless.Allself.deps.<PORT_NAME>must reference a declared Needs port.All declared Needs ports must be reference at least once by any of the methods in the class.All port names must start with a lower-case letter and can only contain alphanumeric characters or underscores.Port names cannot match one of the reserved names, e.g.get_needs,get_provides, etc. For a complete list, seegasofo.ports.RESERVED_PORT_NAMES.Declaring Needs ports as interfacesThe example in the sections above declares Needs ports using a list of port names. This is very convenient and quick,
but is not very IDE or testing friendly.The recommended approach is to declare Needs ports usingNeedsInterface.fromgasofoimportService,NeedsInterface,providesclassMyServiceNeeds(NeedsInterface):defsome_data(self):# type: () -> int"""A brief description."""defanother_service(self,value):# type: (int) -> int"""You can include as much doc here as you like."""classMyService(Service):deps=MyServiceNeeds()@providesdefmy_feature(self,x):data=self.deps.some_data()# needs are accessed via self.deps.<port_name>more_data=self.deps.another_service(value=x)returndata+more_dataBenefits of usingNeedsInterfaceoverNeeds([...]):Attributes ofself.depsare no longer dynamically inject, which means that auto-completion and suggestions in IDE
will now work.The method construct allows for type hinting and docstrings.The function signature is now explicit and will be used by the testing framework to assert that the ports are called
with the expected arguments.The type hinting is optional as far as Gasofo is concerned, but we encourage using it. These ports are only wired to
concrete implementation at run-time, so the type hints is the only reliable way for your IDE infer the type of the
arguments and return values. That extra effort is worth it!Notes on code navigation in PyCharm:The usual 'Find Usages' and 'Go To Declaration' features would work as usual
but this will only allow you to jump between the deps usage and the stubs methods in the NeedsInterface class. The
provider implementation is not statically associated hence not discoverable by PyCharm. The easiest way to locate a
matching provider port would be to use the 'Go to Symbol' feature (Navigate > Symbol) will find all definitions of a
symbol. We recommended creating a custom keymap shortcut for this -- I use super+mouse right click which allows me to
quicky click on any deps or needs stub and locate other definitions.'Using@provides_withWhen we use@providesto define Provides ports, the name of the port will be taken from the method name. In situations
where we want the port names to differ from the actual method name, we can use@provides_with.fromgasofoimportService,provides_withclassMyService(Service):@provides_with('db_get_blah')defget_blah(self,blah_id):# ...The mismatch between the published port name and actual method name could cause confusion, so use this sparingly.@provides_withalso allows us to attached additional metadata (flags) ports, e.g.@provides_with('db_get_blah', web_only=True).We do not currently use these flags, so we will hold off on the docs for now :)Defining DomainsDomains are a collection of components (services or other domains) grouped together and encapsulated to form a higher
level business component. A subset of ports from the containing components are published as the Provides ports of the
domain, and all Needs ports of components that are not fulfilled internally by matching Provides are exposed as the
Needs of the Domain.fromgasofoimportDomainfrommyproject.servicesimportMyService,AnotherServiceclassMyDomain(Domain):__services__=[MyService,AnotherService]# Components contained in this domain__provides__=['get_blah','do_something_else']# subset of ports from services defined in __services__services__should be defined as a list of components (Services or Domains) classes, not instances. An instance of
each of these components will be instantiated when the Domain is instantiated, and the internal ports that have
matching names will be automatically wired together.The Domain class should not contain any other attributes, methods, or a constructor.As with services, ports of a domain can be queried by callingget_needs()andget_provides()on the domain class
or instance.Upon instantiation, proxy methods are dynamically bound to the domain object so the Provides ports can also be accessed
as a method call i.e.my_domain_instance.my_port(...). This is  handy but is not currently very
IDE friendly -- dynamically added methods and the underlying argspec of the port are not known to IDES so code
suggestion and type checking will not work. (We may address this at some point if we find ourselves needing
to access these methods on a regular basis.)Automatically registering Provides ports for domainsFor domains with lots of internal component and lots of intended Provides ports, manually defining them and keeping
them up-to-date can be a chore.For case like this, useAutoProvide:fromgasofoimportDomain,AutoProvidefrommyproject.servicesimportMyService,AnotherServiceclassMyDomain(Domain):__services__=[MyService,AnotherService]# Components contained in this domain__provides__=AutoProvide(pattern='db_.*')# auto export all ports that start with db_Autoprovideallows a convenient way to publish all Provides ports that matches the given regex pattern. If a pattern
is not provided,allprovides ports of internal services are exposed. Please use this sparingly, and always
double-check that you are not exposing more than intended by queryingMyDomain.get_provides().Wiring up an applicationIn the simplest of use cases, one can manually hook up a Needs port by callingservice_instance.deps.connect_port(port_name='blah', func=some_callable). Note that this is an operation on theservice.depsand is connectable to anything callable. Working at this level can get unwieldy once we have more than
a handful of ports in an application.It is therefore recommended that the wiring up if ports is done at a higher level, i.e. at the component level. For
example:c1=MyComponent()# This could be a Service or Domainc2=MyProvider()# Anything that implements IProvide, e.g. Service, Domain, or some custom implementationc1.set_provider(port_name='blah',provider=c2)# c1.deps.blah  ---> c2.blahThe pre-requisite here is that the provider's port name has to match the port name of the consumer. This we believe is
a good thing -- having globally unique port names within the application to denote intent and compatibility makes it
easier to reason about ports and allow for auto-wiring.Auto-wiringIt was mentioned above that, on instantiation, domains will automatically instantiate all underlying services and
auto-wire them based on port names. You can usegasofo.auto_wire()to do the same for components you instantiate
yourself using. This would typically be how you'd wire up a full application.fromgasofoimportauto_wire,Domainfrommyapp.domainsimport*frommyapp.adaptersimport*classMyAwesomeApp(Domain):# encapsulate all my app domain into a single domain__services__=[DomainA,DomainB,DomainC,DomainD]__provides__=LIST_OF_PORTS_TO_EXPOSE_AT_APP_LEVELdefget_app():app=MyAwesomeApp()dependencies=[my_db_provider(),redis_provider(),logging_provider(),]auto_wire([app]+dependencies,expect_all_ports_connected=True)# raise if there are unfulfilled portsreturnappConvenience functions for creating providersAs mentioned above, the recommended approach to wiring is to do so at the component level. This means that any callable
we wish to include in the wiring needs to implement thegasofo.IProvideinterface.This isn't hard to do, but involves unnecessary boiler plate to wrap them up in a compatible class structure.For cases like this, you can useobject_as_providerorfunc_as_providerto automatically wrap an object or function
within a wrapper that exposes theIProvideinterface.Some examples:fromgasofoimportfunc_as_providerimporthashlib# creates provider which provides "get_md5_hash"md5_provider=func_as_provider(func=hashlib.md5,port='get_md5_hash')fromgasofoimportobject_as_providerclassMyStack(object):def__init__(self):self.stack=[]defpush_to_stack(self,value):self.stack.append(value)defpop_from_stack(self):returnself.stack.pop()stack_provider=object_as_provider(provider=MyStack(),ports=['push_to_stack','pop_from_stack'])fromgasofoimportobject_as_provider# we can also expose class methods and static methods as portsclassSerializers(object):@classmethoddefserialise_to_json(cls,payload):# ...@staticmethoddefserialise_to_xml(payload):# ...serialisation_provider=object_as_provider(provider=Serializers,ports=['serialise_to_json','serialise_to_xml',])AdaptersAdapters allows us to inject logic between a port and the provider of that dependency. One way to look at it is that
services should focus on business logic and accesses a port to get data or perform some action. It should not
concern itself with how that dependency is provided or what the structure is at the origin, and instead leave it up to
adapters to handle the more mechanical operations like transport, serialisation/deserialisation, payload transformation, etc.Take for instance a service that provides a certain dataset, and several other services that need that dataset but in
different formats. Instead of having multiple providers ports for the different formats, we could have all consumers
connect to the same provider but each with a different adapter to handle reformatting.Another example would be when moving a service to a different process - we could simply introduce adapters that make
REST or gRPC calls to connections that now span processes with zero changes to the services themselves.In Qwil we use two kinds of adapters:Service-based adaptersInjected adaptersService-based adaptersService based adapters are essentially standard providers i.e. objects that expose INeed and IProvide interfaces. They
are technically no different from Services except that they contain no business logic and instead server as a bridge
between two ports.By ensuring that we use globally unique port names throughout the application, and guaranteeing that ports with matching
names are compatible, we can simply throw in service-based adapters with the corresponding names to handle
incompatibilities and let the auto-wiring process hook them up.For example, say Service A providers port X and this data is needed by service B and C. However, service C needs the
data in a slightly different format. Instead of C declaring a need for X and then pollute its business logic with data
transformation, it should declare the needs with an different port name and rely on an adapter to do the reformatting.+---------+                    
    |         |
    |    B    X -------------------------------+                                        
    |         |                                |      +---------+
    +---------+                                |      |         |
                                               +----> X    A    | 
                                               |      |         |
    +---------+          +---------------+     |      +---------+
    |         |          |               |     |
    |    C    Xy -----> Xy   MyAdapter   X ----+
    |         |          |               |
    +---------+          +---------------+Injected adapters(NOT YET IMPLEMENTED)Injected adapters are call-through callables that are injected when a ports are being connected. This will be done
at wiring time.Injection can be targetted (i.e. inject between connections for specific ports) or app-wide (injected in all
connections). The latter will be used mainly in a debug/dev scenario for instrumenting port calls e.g. for real-time
sequence diagrams, performance analysis, detailed logging.VisualisationVisualisation is important as it will allow us to reason about the application and higher levels of abstraction, and
to visually confirm that components are indeed wired the way we intended.(NOT YET IMPLEMENTED)Domain visualisation (no need to instantiate services/domains)App visualisation (Domains/Services are instantiated and wired up)Real-time sequence diagramsTestingWhen done correctly, apps and components written with Gasofo are very suited to the theArrange-Act-Assert/Given-When-Thenstyle of
testing - since the components are stateless the "Givens" can be defined by simply setting up the Needs ports and the
"Whens" are calls to Provides ports.We should never need tomock.patchanything as long as all dependencies are correctly declared as ports rather than
accessed directly from within the service.See./tests/example/for some examples of how to test components written with gasofo.The basicsFor each test scenario, we should attach only Needs ports that are explicitly needed by the behaviour under test. All
other ports should remain unattached to ensure that tests will fail if an unexpected dependency is accessed.Attaching a port in a test can be done manually, i.e. preparing a provider and assigning it to the service port. For
example, say we have a Clock service defined as:classClock(Service):deps=Needs(['get_current_time'])@providesdeftick(self):dt=self.deps.get_current_time()returndt.strftime('%Y-%m-%d%H:%M')We could test this as such:classClockTest(unittest.TestCase):deftest_tick_returns_formatted_time(self):clock_service=Clock()# GIVEN the current date time is datetime.datetime(2018, 9, 20, 14, 55)datetime_provider=func_as_provider(func=lambda:datetime.datetime(2018,9,20,14,55),port='get_current_time')clock_service.set_provider('get_current_time',datetime_provider)# WHEN tick() is calledresult=clock_service.tick()# THEN '2018-09-20 14:55' is returnedself.assertEqual('2018-09-20 14:55',result)This will work and is reasonably clean, but does require quite a bit of boilerplate code. We can simplify this further
by usinggasofo.testing.attach_mock_provider.gasofo.testing.attach_mock_providerThis is a handy way for generating a provider which can satisfy one or more ports of a service. Using this helper,
the test above could be rewritten as:fromgasofo.testingimportattach_mock_providerclassClockTest(unittest.TestCase):clock_service=Clock()# GIVEN the current date time is datetime.datetime(2018, 9, 20, 14, 55)attach_mock_provider(consumer=clock_service,ports={'get_current_time':datetime.datetime(2018,9,20,14,55),# return value when port is called})# WHEN tick() is calledresult=clock_service.tick()# THEN '2018-09-20 14:55' is returnedself.assertEqual('2018-09-20 14:55',result)attach_mock_providergenerates a provider object which offers ports as defined in theportsargument, then attaches
the consuming component to this provider. Any ports on the consumer that is not defined in the call will remain
unattached.attach_mock_provideralso returns the provider object where all generated mock ports are accessible as attributes on
this object. These attributes are instances ofmock.Mockobjects which allows us to do more elaborate test setup, e.g.provider=attach_mock_provider(consumer=some_service,ports=['get_a','get_b'])provider.get_a.return_value=datetime.datetime(2018,9,20,14,55)# can set .return_value as usualprovider.get_b.side_effect={'a':1,'b'=2}.get# get_b(x) calls {'a':1, 'b'=2}.get(x)some_service.do_blah()provider.get_b.assert_called_once_with(2)# can be treated like any a standard mock.Mock objectNote that theportsargument above is declared as a list instead of a dict. This does the same thing except that thereturn_valueof the mock is not set by default.An extra benefit to usingattach_mock_provideris that if the component Needs are defined as aNeedsInterfaceinstance, then the underling mock objects for the ports are created usingmock.create_autospec. This will assert that
all calls to it abide by the argspec of the needs port, thereby validating that service methods are accessing deps
as expected.(The only thing missing for now to complete this picture is wiring-time assertion that connected needs and
provides port have compatible argspecs).Given-When-ThenTo write even more succinct tests, one can also use theGasofoTestCasebase class wraps away most of the test setup
and provides the ability to construct tests as a series of GIVEN-WHEN-THEN calls.For example, to test theClockservice defined abovefromgasofo.testingimportGasofoTestCaseclassClockTest(GasofoTestCase):SERVICE_CLASS=Clock# service under testdeftest_tick_returns_formatted_time(self):self.GIVEN(needs_port='get_current_time',returns=datetime.datetime(2018,9,20,14,55))self.WHEN(port_called='tick')# this also takes kwargs which will all be passed to the port callself.THEN(expected_output='2018-09-20 14:55')It is worth noting that theself.GIVENcall returns the created mock object while theself.WHENcall returns the
actual output of the port call.Do also explore the other arguments support byself.GIVENandself.THENas they provide means for declaring more
complex requirements, e.g. setting up side effects for GIVENs or specifying that we do not care about the order of the
expected output.GasofoTestCasealso provides assertions methods to assert that the needs ports are called as expected. This can be a
simple assertion, or a more involved assertion that the dictates the order in which the needs ports must be
called. For example:# example taken from tests/example/domains/coffee_orders/test_orders_service.pyself.assert_ports_called(calls=[PortCall(port='db_get_active_order',kwargs={'room':'Le trou des chouettes'}),PortCall(port='is_valid_menu_item',kwargs={'item_name':'Flat White'}),PortCall(port='db_add_order_item',kwargs={'room':'Le trou des chouettes','item':'Flat White','recipient':'Shawn',}),])For more examples, seetests/example/domains/coffee_orders/test_order_history_service.py. Both the tests classes
defined in this file --OrderHistoryServiceTestSimplifiedandOrderHistoryServiceTestWithoutFramework-- are
equivalent but with the latter implemented withoutGasofoTestCase.Higher level testing i.e. domains, app, integration, acceptance testingWriting tests for domains is identical to testing services since they all implement the same interfaces.Testing at the app level, as well as integration/acceptance testing can also be expressed in similar forms except that
the setup for the tests would be more elaborate. For example, one might wire up the full application without the edge
dependencies, then treat the whole mesh as a single domain. We could then use the same tooling as described above to
implement our acceptance tests or integration tests.Seeexample/domains/test_app.pyfor a simple example of how this might be achieved.