{ "info": { "author": "Shawn Chin", "author_email": "shawn@qwil.io", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7" ], "description": "# Gasofo (framework for Qwil's hexagonal code architecture)\n\nGasofo is Qwil's take on a implementing a Hexagonal code architecture\n([1](https://marcus-biel.com/hexagonal-architecture/), \n[2](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html), \n[3](https://martinfowler.com/bliki/PresentationDomainDataLayering.html),\n[4](https://engineering.laterooms.com/hexagonal-architecture-in-practice/)). \n\n\nSee `./example` for an example of how gasofo can be used to build up an app.\n\n## Installing Gasofo\n\n\n`pip install gasofo`\n\n\n## Defining Services\n\nServices should be stateless and should only access resources other services via its \"Needs\" ports. Functionality \nprovided by a service are exposed via \"Provides\" ports.\n\nA Service can be defined as such:\n\n```python\nfrom gasofo import Service, Needs, provides\n\nclass MyService(Service):\n\n deps = Needs(['some_data', 'another_service']) # ports this service 'Needs'\n\n @provides\n def my_feature(self, x):\n data = self.deps.some_data() # needs are accessed via self.deps.\n more_data = self.deps.another_service(value=x)\n return data + more_data\n```\n\nHere we defined the service `MyService` with a couple of Needs ports and a single Provides port named \n\"my_feature\". \n\nMethods on instances of this class can be called just like a regular class, but only the ones tagged \nwith `@provides` are discoverable as a port.\n\nDependencies of the service are access through the Needs port via `self.deps.PORT_NAME(...)`. These ports will be \ninjected with actual provider functions when the application is wired up. Calling a port before it is connected to a\nprovider will raise a `DisconnectedPort` exception.\n\nThe ports of a service can be queried by calling `get_needs()` and `get_provides()` on the service class or instance.\nThis helps with the visualisation and auto-wiring of Services to form business domains and complete applications.\n\n\n### Validations on declaration\n\nExceptions will be raised during class construction (which usually means when you import the module) if the following\nvalidations fail:\n* Constructors (`__init__`) are not allowed since services are meant to be stateless.\n* All `self.deps.` must reference a declared Needs port.\n* All declared Needs ports must be reference at least once by any of the methods in the class.\n* All port names must start with a lower-case letter and can only contain alphanumeric characters or underscores.\n* Port names cannot match one of the reserved names, e.g. `get_needs`, `get_provides`, etc. For a complete list, see\n `gasofo.ports.RESERVED_PORT_NAMES`.\n\n\n### Declaring Needs ports as interfaces\n\nThe example in the sections above declares Needs ports using a list of port names. This is very convenient and quick,\nbut is not very IDE or testing friendly. \n\nThe recommended approach is to declare Needs ports using `NeedsInterface`.\n```python\nfrom gasofo import Service, NeedsInterface, provides\n\n\nclass MyServiceNeeds(NeedsInterface):\n\n def some_data(self):\n # type: () -> int\n \"\"\"A brief description.\"\"\"\n\n def another_service(self, value):\n # type: (int) -> int\n \"\"\"You can include as much doc here as you like.\"\"\"\n\n\nclass MyService(Service):\n\n deps = MyServiceNeeds()\n\n @provides\n def my_feature(self, x):\n data = self.deps.some_data() # needs are accessed via self.deps.\n more_data = self.deps.another_service(value=x)\n return data + more_data\n```\n\nBenefits of using `NeedsInterface` over `Needs([...])`:\n* Attributes of `self.deps` are no longer dynamically inject, which means that auto-completion and suggestions in IDE\n will now work.\n* The method construct allows for type hinting and docstrings.\n* The function signature is now explicit and will be used by the testing framework to assert that the ports are called\n with the expected arguments.\n\nThe type hinting is optional as far as Gasofo is concerned, but we encourage using it. These ports are only wired to \nconcrete implementation at run-time, so the type hints is the only reliable way for your IDE infer the type of the\narguments and return values. That extra effort is worth it!\n\n_**Notes on code navigation in PyCharm:** The usual 'Find Usages' and 'Go To Declaration' features would work as usual\nbut this will only allow you to jump between the deps usage and the stubs methods in the NeedsInterface class. The \nprovider implementation is not statically associated hence not discoverable by PyCharm. The easiest way to locate a\nmatching provider port would be to use the 'Go to Symbol' feature (Navigate > Symbol) will find all definitions of a \nsymbol. We recommended creating a custom keymap shortcut for this -- I use super+mouse right click which allows me to \nquicky click on any deps or needs stub and locate other definitions.'_\n\n### Using `@provides_with`\n\nWhen we use `@provides` to define Provides ports, the name of the port will be taken from the method name. In situations\nwhere we want the port names to differ from the actual method name, we can use `@provides_with`.\n\n```python\nfrom gasofo import Service, provides_with\n\nclass MyService(Service):\n\n @provides_with('db_get_blah')\n def get_blah(self, blah_id):\n # ...\n```\n\nThe mismatch between the published port name and actual method name could cause confusion, so use this sparingly.\n\n`@provides_with` also allows us to attached additional metadata (flags) ports, e.g. \n`@provides_with('db_get_blah', web_only=True)`. \n\nWe do not currently use these flags, so we will hold off on the docs for now :)\n\n\n## Defining Domains\n\nDomains are a collection of components (services or other domains) grouped together and encapsulated to form a higher \nlevel business component. A subset of ports from the containing components are published as the Provides ports of the \ndomain, and all Needs ports of components that are not fulfilled internally by matching Provides are exposed as the\nNeeds of the Domain.\n\n```python\nfrom gasofo import Domain\nfrom myproject.services import MyService, AnotherService\n\nclass MyDomain(Domain):\n __services__ = [MyService, AnotherService] # Components contained in this domain\n __provides__ = ['get_blah', 'do_something_else'] # subset of ports from services defined in __services\n```\n\n`__services__` should be defined as a list of components (Services or Domains) classes, not instances. An instance of \neach of these components will be instantiated when the Domain is instantiated, and the internal ports that have \nmatching names will be automatically wired together.\n\nThe Domain class should not contain any other attributes, methods, or a constructor.\n\nAs with services, ports of a domain can be queried by calling `get_needs()` and `get_provides()` on the domain class \nor instance.\n\nUpon instantiation, proxy methods are dynamically bound to the domain object so the Provides ports can also be accessed\nas a method call i.e. `my_domain_instance.my_port(...)`. This is handy but is not currently very\nIDE friendly -- dynamically added methods and the underlying argspec of the port are not known to IDES so code\nsuggestion and type checking will not work. (We may address this at some point if we find ourselves needing \nto access these methods on a regular basis.)\n\n### Automatically registering Provides ports for domains\n\nFor domains with lots of internal component and lots of intended Provides ports, manually defining them and keeping\nthem up-to-date can be a chore.\n\nFor case like this, use `AutoProvide`:\n\n```python\nfrom gasofo import Domain, AutoProvide\nfrom myproject.services import MyService, AnotherService\n\nclass MyDomain(Domain):\n __services__ = [MyService, AnotherService] # Components contained in this domain\n __provides__ = AutoProvide(pattern='db_.*') # auto export all ports that start with db_\n```\n\n`Autoprovide` allows a convenient way to publish all Provides ports that matches the given regex pattern. If a pattern\nis not provided, **all** provides ports of internal services are exposed. Please use this sparingly, and always \ndouble-check that you are not exposing more than intended by querying `MyDomain.get_provides()`.\n\n\n## Wiring up an application\n\nIn the simplest of use cases, one can manually hook up a Needs port by calling \n`service_instance.deps.connect_port(port_name='blah', func=some_callable)`. Note that this is an operation on the\n `service.deps` and is connectable to anything callable. Working at this level can get unwieldy once we have more than \n a handful of ports in an application.\n\nIt is therefore recommended that the wiring up if ports is done at a higher level, i.e. at the component level. For\nexample:\n\n```python\nc1 = MyComponent() # This could be a Service or Domain \nc2 = MyProvider() # Anything that implements IProvide, e.g. Service, Domain, or some custom implementation\n\nc1.set_provider(port_name='blah', provider=c2) # c1.deps.blah ---> c2.blah\n```\n\nThe pre-requisite here is that the provider's port name has to match the port name of the consumer. This we believe is \na good thing -- having globally unique port names within the application to denote intent and compatibility makes it\neasier to reason about ports and allow for auto-wiring.\n\n### Auto-wiring\n\nIt was mentioned above that, on instantiation, domains will automatically instantiate all underlying services and \nauto-wire them based on port names. You can use `gasofo.auto_wire()` to do the same for components you instantiate \nyourself using. This would typically be how you'd wire up a full application.\n\n```python\nfrom gasofo import auto_wire, Domain\nfrom myapp.domains import *\nfrom myapp.adapters import *\n\nclass MyAwesomeApp(Domain): # encapsulate all my app domain into a single domain\n __services__ = [DomainA, DomainB, DomainC, DomainD]\n __provides__ = LIST_OF_PORTS_TO_EXPOSE_AT_APP_LEVEL\n\ndef get_app():\n app = MyAwesomeApp()\n dependencies = [\n my_db_provider(),\n redis_provider(),\n logging_provider(),\n ]\n\n auto_wire([app] + dependencies, expect_all_ports_connected=True) # raise if there are unfulfilled ports\n return app\n``` \n\n### Convenience functions for creating providers\n\nAs mentioned above, the recommended approach to wiring is to do so at the component level. This means that any callable\nwe wish to include in the wiring needs to implement the `gasofo.IProvide` interface.\n\nThis isn't hard to do, but involves unnecessary boiler plate to wrap them up in a compatible class structure.\n\nFor cases like this, you can use `object_as_provider` or `func_as_provider` to automatically wrap an object or function\nwithin a wrapper that exposes the `IProvide` interface.\n\nSome examples:\n\n```python\nfrom gasofo import func_as_provider\nimport hashlib\n\n# creates provider which provides \"get_md5_hash\"\nmd5_provider = func_as_provider(func=hashlib.md5, port='get_md5_hash') \n```\n\n```python\nfrom gasofo import object_as_provider\n\nclass MyStack(object):\n def __init__(self):\n self.stack = []\n\n def push_to_stack(self, value):\n self.stack.append(value)\n\n def pop_from_stack(self):\n return self.stack.pop()\n\nstack_provider = object_as_provider(provider=MyStack(), ports=['push_to_stack', 'pop_from_stack'])\n```\n\n```python\nfrom gasofo import object_as_provider\n\n# we can also expose class methods and static methods as ports\nclass Serializers(object):\n\n @classmethod \n def serialise_to_json(cls, payload):\n # ...\n\n @staticmethod\n def serialise_to_xml(payload):\n # ...\n\nserialisation_provider = object_as_provider(provider=Serializers, ports=[\n 'serialise_to_json', \n 'serialise_to_xml',\n])\n```\n\n\n## Adapters\n\nAdapters allows us to inject logic between a port and the provider of that dependency. One way to look at it is that\nservices should focus on business logic and accesses a port to get data or perform some action. It should not \nconcern itself with how that dependency is provided or what the structure is at the origin, and instead leave it up to\nadapters to handle the more mechanical operations like transport, serialisation/deserialisation, payload transformation, etc.\n\nTake for instance a service that provides a certain dataset, and several other services that need that dataset but in \ndifferent formats. Instead of having multiple providers ports for the different formats, we could have all consumers\nconnect to the same provider but each with a different adapter to handle reformatting.\n\nAnother example would be when moving a service to a different process - we could simply introduce adapters that make \nREST or gRPC calls to connections that now span processes with zero changes to the services themselves.\n\nIn Qwil we use two kinds of adapters:\n1. Service-based adapters\n2. Injected adapters\n\n\n### Service-based adapters\n\nService based adapters are essentially standard providers i.e. objects that expose INeed and IProvide interfaces. They \nare technically no different from Services except that they contain no business login and instead server as a bridge\nbetween two ports.\n\nBy ensuring that we use globally unique port names throughout the application, and guaranteeing that ports with matching\nnames are compatible, we can simply throw in service-based adapters with the corresponding names to handle \nincompatibilities and let the auto-wiring process hook them up.\n\nFor example, say Service A providers port X and this data is needed by service B and C. However, service C needs the\ndata in a slightly different format. Instead of C declaring a need for X and then pollute its business logic with data\ntransformation, it should declare the needs with an different port name and rely on an adapter to do the reformatting.\n\n```\n +---------+ \n | |\n | B X -------------------------------+ \n | | | +---------+\n +---------+ | | |\n +----> X A | \n | | |\n +---------+ +---------------+ | +---------+\n | | | | |\n | C Xy -----> Xy MyAdapter X ----+\n | | | |\n +---------+ +---------------+ \n\n```\n\n### Injected adapters\n\n(NOT YET IMPLEMENTED)\n\nInjected adapters are call-through callables that are injected when a ports are being connected. This will be done \nat wiring time.\n\n\nInjection can be targetted (i.e. inject between connections for specific ports) or app-wide (injected in all \nconnections). The latter will be used mainly in a debug/dev scenario for instrumenting port calls e.g. for real-time\nsequence diagrams, performance analysis, detailed logging.\n\n# Visualisation\n\nVisualisation is important as it will allow us to reason about the application and higher levels of abstraction, and\nto visually confirm that components are indeed wired the way we intended.\n\n(NOT YET IMPLEMENTED)\n\n* Domain visualisation (no need to instantiate services/domains)\n* App visualisation (Domains/Services are instantiated and wired up)\n* Real-time sequence diagrams\n\n\n## Testing \n\nWhen done correctly, apps and components written with Gasofo are very suited to the the \n[Arrange-Act-Assert](http://wiki.c2.com/?ArrangeActAssert) / \n[Given-When-Then](https://martinfowler.com/bliki/GivenWhenThen.html) style of \ntesting - since the components are stateless the \"Givens\" can be defined by simply setting up the Needs ports and the \n\"Whens\" are calls to Provides ports.\n\nWe should never need to `mock.patch` anything as long as all dependencies are correctly declared as ports rather than\naccessed directly from within the service. \n\nSee `./tests/example/` for some examples of how to test components written with gasofo.\n\n### The basics\n\nFor each test scenario, we should attach only Needs ports that are explicitly needed by the behaviour under test. All \nother ports should remain unattached to ensure that tests will fail if an unexpected dependency is accessed.\n\nAttaching a port in a test can be done manually, i.e. preparing a provider and assigning it to the service port. For\nexample, say we have a Clock service defined as:\n\n```python\nclass Clock(Service):\n deps = Needs(['get_current_time'])\n\n @provides\n def tick(self):\n dt = self.deps.get_current_time()\n return dt.strftime('%Y-%m-%d %H:%M')\n```\n\nWe could test this as such:\n```python\nclass ClockTest(unittest.TestCase):\n\n def test_tick_returns_formatted_time(self):\n clock_service = Clock()\n\n # GIVEN the current date time is datetime.datetime(2018, 9, 20, 14, 55)\n datetime_provider = func_as_provider(\n func=lambda: datetime.datetime(2018, 9, 20, 14, 55),\n port='get_current_time'\n )\n clock_service.set_provider('get_current_time', datetime_provider)\n\n # WHEN tick() is called\n result = clock_service.tick()\n\n # THEN '2018-09-20 14:55' is returned\n self.assertEqual('2018-09-20 14:55', result)\n```\n\nThis will work and is reasonably clean, but does require quite a bit of boilerplate code. We can simplify this further\nby using `gasofo.testing.attach_mock_provider`.\n\n### `gasofo.testing.attach_mock_provider`\n\nThis is a handy way for generating a provider which can satisfy one or more ports of a service. Using this helper, \nthe test above could be rewritten as:\n\n```python\nfrom gasofo.testing import attach_mock_provider\n\nclass ClockTest(unittest.TestCase):\n clock_service = Clock()\n\n # GIVEN the current date time is datetime.datetime(2018, 9, 20, 14, 55)\n attach_mock_provider(consumer=clock_service, ports={\n 'get_current_time': datetime.datetime(2018, 9, 20, 14, 55), # return value when port is called\n })\n\n # WHEN tick() is called\n result = clock_service.tick()\n\n # THEN '2018-09-20 14:55' is returned\n self.assertEqual('2018-09-20 14:55', result)\n```\n\n`attach_mock_provider` generates a provider object which offers ports as defined in the `ports` argument, then attaches \nthe consuming component to this provider. Any ports on the consumer that is not defined in the call will remain \nunattached.\n\n`attach_mock_provider` also returns the provider object where all generated mock ports are accessible as attributes on \nthis object. These attributes are instances of `mock.Mock` objects which allows us to do more elaborate test setup, e.g.\n\n```python\nprovider = attach_mock_provider(consumer=some_service, ports=['get_a', 'get_b']) \nprovider.get_a.return_value = datetime.datetime(2018, 9, 20, 14, 55) # can set .return_value as usual\nprovider.get_b.side_effect = {'a':1, 'b'=2}.get # get_b(x) calls {'a':1, 'b'=2}.get(x)\n\nsome_service.do_blah()\n\nprovider.get_b.assert_called_once_with(2) # can be treated like any a standard mock.Mock object\n```\n\nNote that the `ports` argument above is declared as a list instead of a dict. This does the same thing except that the\n`return_value` of the mock is not set by default.\n\nAn extra benefit to using `attach_mock_provider` is that if the component Needs are defined as a `NeedsInterface` \ninstance, then the underling mock objects for the ports are created using `mock.create_autospec`. This will assert that \nall calls to it abide by the argspec of the needs port, thereby validating that service methods are accessing deps\nas expected. _(The only thing missing for now to complete this picture is wiring-time assertion that connected needs and \nprovides port have compatible argspecs)_.\n\n### Given-When-Then\n\nTo write even more succinct tests, one can also use the `GasofoTestCase` base class wraps away most of the test setup\nand provides the ability to construct tests as a series of GIVEN-WHEN-THEN calls.\n\nFor example, to test the `Clock` service defined above\n```python\nfrom gasofo.testing import GasofoTestCase\n\nclass ClockTest(GasofoTestCase):\n SERVICE_CLASS = Clock # service under test\n\n def test_tick_returns_formatted_time(self):\n self.GIVEN(needs_port='get_current_time', returns=datetime.datetime(2018, 9, 20, 14, 55))\n self.WHEN(port_called='tick') # this also takes kwargs which will all be passed to the port call\n self.THEN(expected_output='2018-09-20 14:55')\n```\n\nIt is worth noting that the `self.GIVEN` call returns the created mock object while the `self.WHEN` call returns the \nactual output of the port call.\n\nDo also explore the other arguments support by `self.GIVEN` and `self.THEN` as they provide means for declaring more\ncomplex requirements, e.g. setting up side effects for GIVENs or specifying that we do not care about the order of the \nexpected output.\n\n`GasofoTestCase` also provides assertions methods to assert that the needs ports are called as expected. This can be a\nsimple assertion, or a more involved assertion that the dictates the order in which the needs ports must be \ncalled. For example:\n\n```python\n# example taken from tests/example/domains/coffee_orders/test_orders_service.py\n\nself.assert_ports_called(calls=[\n GasofoTestCase.PortCalled(port='db_get_active_order', kwargs={'room': 'Le trou des chouettes'}),\n GasofoTestCase.PortCalled(port='is_valid_menu_item', kwargs={'item_name': 'Flat White'}),\n GasofoTestCase.PortCalled(port='db_add_order_item', kwargs={\n 'room': 'Le trou des chouettes',\n 'item': 'Flat White',\n 'recipient': 'Shawn',\n }),\n])\n```\n\nFor more examples, see `tests/example/domains/coffee_orders/test_order_history_service.py`. Both the tests classes \ndefined in this file -- `OrderHistoryServiceTestSimplified` and `OrderHistoryServiceTestWithoutFramework` -- are \nequivalent but with the latter implemented without `GasofoTestCase`.\n\n\n### Higher level testing i.e. domains, app, integration, acceptance testing\n\nWriting tests for domains is identical to testing services since they all implement the same interfaces. \n\nTesting at the app level, as well as integration/acceptance testing can also be expressed in similar forms except that\nthe setup for the tests would be more elaborate. For example, one might wire up the full application without the edge\ndependencies, then treat the whole mesh as a single domain. We could then use the same tooling as described above to \nimplement our acceptance tests or integration tests.\n\nSee `example/domains/test_app.py` for a simple example of how this might be achieved.\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/QwilApp/gasofo", "keywords": "", "license": "", "maintainer": "", "maintainer_email": "", "name": "gasofo", "package_url": "https://pypi.org/project/gasofo/", "platform": "", "project_url": "https://pypi.org/project/gasofo/", "project_urls": { "Homepage": "https://github.com/QwilApp/gasofo" }, "release_url": "https://pypi.org/project/gasofo/1.0.3.1/", "requires_dist": null, "requires_python": "", "summary": "Qwil's hexagonal architecture framework", "version": "1.0.3.1" }, "last_serial": 5635163, "releases": { "1.0.1": [ { "comment_text": "", "digests": { "md5": "ef283fed1e506b48d0236c903d24df96", "sha256": "1e688844b1b573a996fcac93a5598341708514e85833773686b367cb8cb25eee" }, "downloads": -1, "filename": "gasofo-1.0.1-py2-none-any.whl", "has_sig": false, "md5_digest": "ef283fed1e506b48d0236c903d24df96", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 22820, "upload_time": "2019-07-17T10:08:57", "url": "https://files.pythonhosted.org/packages/65/fe/b937c14236ba83e22b08a2344f06162dc71dc6eb32dc579696d5247ad9cd/gasofo-1.0.1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "dfbacfb4c7c5b279d98eee59799c4575", "sha256": "2d9b6c9de0b36e0d9f00ff257949d0e777ba4b042208e38f22ccd87237b0bc90" }, "downloads": -1, "filename": "gasofo-1.0.1.tar.gz", "has_sig": false, "md5_digest": "dfbacfb4c7c5b279d98eee59799c4575", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19999, "upload_time": "2019-07-17T10:09:00", "url": "https://files.pythonhosted.org/packages/f3/18/483e02d6ad029543cd38989e230875f343dcf54f3f61afa6be0bea0e3e47/gasofo-1.0.1.tar.gz" } ], "1.0.1.post1": [ { "comment_text": "", "digests": { "md5": "0b1621441d6dc5566dc74177a34eda5f", "sha256": "016f37a27591f0c19851ddfbf081fc6a5a4a872528cfd9429775b8dc65d0a58f" }, "downloads": -1, "filename": "gasofo-1.0.1.post1-py2-none-any.whl", "has_sig": false, "md5_digest": "0b1621441d6dc5566dc74177a34eda5f", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 22840, "upload_time": "2019-07-17T14:24:09", "url": "https://files.pythonhosted.org/packages/6b/ce/21318ad86e76831f9b3dd5a3848d3f9f222e1d4d31b5b024d0f3da8e0609/gasofo-1.0.1.post1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "51425cea97f0313e74cdddd7b6559ebf", "sha256": "d31d83625ba622d3c03fcaac2f5c8d0bf68988e5efd0c036bbf3cce6bf855799" }, "downloads": -1, "filename": "gasofo-1.0.1.post1.tar.gz", "has_sig": false, "md5_digest": "51425cea97f0313e74cdddd7b6559ebf", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19983, "upload_time": "2019-07-17T14:24:11", "url": "https://files.pythonhosted.org/packages/f4/b1/7b1cfb4fbdd21b9e7249c52670cb4f54a019b76b30db8fbc469f5a50f032/gasofo-1.0.1.post1.tar.gz" } ], "1.0.2": [ { "comment_text": "", "digests": { "md5": "4b7255ce712301d1a85c22c3cacd3350", "sha256": "eb49ac5aa00c7ac3961988b7db530bf97208657bf27174e5ed8d340278973b30" }, "downloads": -1, "filename": "gasofo-1.0.2-py2-none-any.whl", "has_sig": false, "md5_digest": "4b7255ce712301d1a85c22c3cacd3350", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 24302, "upload_time": "2019-07-29T09:33:01", "url": "https://files.pythonhosted.org/packages/2f/67/a3c91841f9501bfced1e6ac3609b1590d0e647ff59bafc67123ce9d2932b/gasofo-1.0.2-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "e0e4392ae7567cce2ca96e84a350d37e", "sha256": "0b1fe2a4927a6e523ee70f8b108dd4ef938cbe137f0179644e6cc3abc3c4ef6f" }, "downloads": -1, "filename": "gasofo-1.0.2.tar.gz", "has_sig": false, "md5_digest": "e0e4392ae7567cce2ca96e84a350d37e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 21011, "upload_time": "2019-07-29T09:33:06", "url": "https://files.pythonhosted.org/packages/36/9f/a614fdfe5d1695a3be58343bbdbc219155691324889d381d63e194439c5e/gasofo-1.0.2.tar.gz" } ], "1.0.2.1": [ { "comment_text": "", "digests": { "md5": "3fb0a61e49102cec599a6c572ec6cc2f", "sha256": "ebcf0f2588d488e7e1ff7c71f7205d8a44e5f52f5c4e620621fabaf7553056ad" }, "downloads": -1, "filename": "gasofo-1.0.2.1-py2-none-any.whl", "has_sig": false, "md5_digest": "3fb0a61e49102cec599a6c572ec6cc2f", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 24329, "upload_time": "2019-07-29T16:42:45", "url": "https://files.pythonhosted.org/packages/53/b4/00d0de19216f4fa4dbba59332fce1c93a7847897a09b86fa1bcdf25e71be/gasofo-1.0.2.1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "9605131d303144f473cc03680ddcc0c2", "sha256": "05263c89209a58c12a1dbb667ef4f3e378b0fc6810ed786a9894fe3c67afa43b" }, "downloads": -1, "filename": "gasofo-1.0.2.1.tar.gz", "has_sig": false, "md5_digest": "9605131d303144f473cc03680ddcc0c2", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 21030, "upload_time": "2019-07-29T16:42:48", "url": "https://files.pythonhosted.org/packages/7a/96/8abf2cfa6161b2d29546596ca018e187cf8310de36c03d8b3bdd68e2d379/gasofo-1.0.2.1.tar.gz" } ], "1.0.3": [ { "comment_text": "", "digests": { "md5": "364ecbeee6e2d7652eb352bcfbca865e", "sha256": "45c2c1bc34e4005b4547fc08e80e7ec0707c241db8b3ca2e16f6439abba3f11c" }, "downloads": -1, "filename": "gasofo-1.0.3-py2-none-any.whl", "has_sig": false, "md5_digest": "364ecbeee6e2d7652eb352bcfbca865e", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 24546, "upload_time": "2019-07-30T15:34:31", "url": "https://files.pythonhosted.org/packages/17/8b/141449951c147bbe8d8af6f7da93357a28307ea3bc104f59a3d7791814f8/gasofo-1.0.3-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "a4cc7771c4f7b65f503252ea40e2bfc3", "sha256": "5f0f3bfa5443de21396fc127b5beb2424df3a320b6c1bd42b69d6760255e7b98" }, "downloads": -1, "filename": "gasofo-1.0.3.tar.gz", "has_sig": false, "md5_digest": "a4cc7771c4f7b65f503252ea40e2bfc3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 21259, "upload_time": "2019-07-30T15:34:34", "url": "https://files.pythonhosted.org/packages/7b/50/ae757125b10b7ee60df93e1685530815acf96af71f30b1faf42d0bbf2a07/gasofo-1.0.3.tar.gz" } ], "1.0.3.1": [ { "comment_text": "", "digests": { "md5": "be76f6a63e1fbd293fe241b68f7d2916", "sha256": "e6d134cb649c46faebc231221ee63e89eae8217a547909c723a9d1a7843849fc" }, "downloads": -1, "filename": "gasofo-1.0.3.1-py2-none-any.whl", "has_sig": false, "md5_digest": "be76f6a63e1fbd293fe241b68f7d2916", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 24636, "upload_time": "2019-08-05T16:11:07", "url": "https://files.pythonhosted.org/packages/a6/1f/302a59ab0e3513f39931a5c76adfc561e38971bd41372d57eea827074e63/gasofo-1.0.3.1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0477119e943d819f4bdc6c210aa6223e", "sha256": "f591a6a175e7d0d3a7d60705cb647ed96c2ae0d1616463a7b6a7d5e905d8ef36" }, "downloads": -1, "filename": "gasofo-1.0.3.1.tar.gz", "has_sig": false, "md5_digest": "0477119e943d819f4bdc6c210aa6223e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 21449, "upload_time": "2019-08-05T16:11:09", "url": "https://files.pythonhosted.org/packages/56/ae/934c62c7ffb31454476dd1d70c4ae046d1fa4a1dc776647782f44804d41a/gasofo-1.0.3.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "be76f6a63e1fbd293fe241b68f7d2916", "sha256": "e6d134cb649c46faebc231221ee63e89eae8217a547909c723a9d1a7843849fc" }, "downloads": -1, "filename": "gasofo-1.0.3.1-py2-none-any.whl", "has_sig": false, "md5_digest": "be76f6a63e1fbd293fe241b68f7d2916", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 24636, "upload_time": "2019-08-05T16:11:07", "url": "https://files.pythonhosted.org/packages/a6/1f/302a59ab0e3513f39931a5c76adfc561e38971bd41372d57eea827074e63/gasofo-1.0.3.1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0477119e943d819f4bdc6c210aa6223e", "sha256": "f591a6a175e7d0d3a7d60705cb647ed96c2ae0d1616463a7b6a7d5e905d8ef36" }, "downloads": -1, "filename": "gasofo-1.0.3.1.tar.gz", "has_sig": false, "md5_digest": "0477119e943d819f4bdc6c210aa6223e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 21449, "upload_time": "2019-08-05T16:11:09", "url": "https://files.pythonhosted.org/packages/56/ae/934c62c7ffb31454476dd1d70c4ae046d1fa4a1dc776647782f44804d41a/gasofo-1.0.3.1.tar.gz" } ] }