├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── source │ ├── api │ │ └── index.rst │ ├── architecture │ │ ├── event_based_layout.png │ │ ├── http_internal.png │ │ ├── http_related_cascade.png │ │ ├── http_request.png │ │ ├── http_single_cascade.png │ │ ├── index.rst │ │ ├── kafka_api_event.png │ │ ├── kafka_final_cascade.png │ │ ├── kafka_first_pending_simple.png │ │ ├── kafka_reply.png │ │ ├── layout.png │ │ └── service_join.png │ ├── auth │ │ └── index.rst │ ├── conf.py │ ├── handlers │ │ └── index.rst │ ├── index.rst │ ├── internals │ │ ├── index.rst │ │ ├── nautilus.api.endpoints.requestHandlers.rst │ │ ├── nautilus.api.endpoints.rst │ │ ├── nautilus.api.rst │ │ ├── nautilus.api.util.rst │ │ ├── nautilus.auth.models.fields.rst │ │ ├── nautilus.auth.models.mixins.rst │ │ ├── nautilus.auth.models.rst │ │ ├── nautilus.auth.primitives.rst │ │ ├── nautilus.auth.requestHandlers.forms.rst │ │ ├── nautilus.auth.requestHandlers.rst │ │ ├── nautilus.auth.rst │ │ ├── nautilus.config.rst │ │ ├── nautilus.contrib.graphene_peewee.rst │ │ ├── nautilus.contrib.rst │ │ ├── nautilus.conventions.rst │ │ ├── nautilus.management.rst │ │ ├── nautilus.management.scripts.rst │ │ ├── nautilus.management.util.rst │ │ ├── nautilus.models.fields.rst │ │ ├── nautilus.models.rst │ │ ├── nautilus.models.serializers.rst │ │ ├── nautilus.network.events.actionHandlers.rst │ │ ├── nautilus.network.events.consumers.rst │ │ ├── nautilus.network.events.rst │ │ ├── nautilus.network.http.rst │ │ ├── nautilus.network.rst │ │ └── nautilus.services.rst │ ├── intro │ │ ├── api.rst │ │ ├── auth.rst │ │ ├── connecting_services.rst │ │ ├── first_model.rst │ │ ├── first_service.rst │ │ ├── index.rst │ │ └── installation.rst │ ├── services │ │ └── index.rst │ └── utilities │ │ └── index.rst └── sphinx.make ├── example ├── api.py ├── ingredients.py ├── recipeIngredients.py └── recipes.py ├── nautilus ├── __init__.py ├── api │ ├── __init__.py │ ├── endpoints │ │ ├── __init__.py │ │ ├── requestHandlers │ │ │ ├── __init__.py │ │ │ ├── apiQuery.py │ │ │ ├── graphiql.py │ │ │ └── graphql.py │ │ ├── static │ │ │ ├── build │ │ │ │ ├── scripts │ │ │ │ │ └── graphiql.js │ │ │ │ └── styles │ │ │ │ │ └── graphiql.css │ │ │ └── src │ │ │ │ ├── scripts │ │ │ │ └── graphiql.js │ │ │ │ └── styles │ │ │ │ └── graphiql.css │ │ └── templates │ │ │ └── graphiql.html │ ├── filter.py │ ├── schema.py │ └── util │ │ ├── __init__.py │ │ ├── arg_string_from_dict.py │ │ ├── build_native_type_dictionary.py │ │ ├── convert_typestring_to_api_native.py │ │ ├── create_model_schema.py │ │ ├── fields_for_model.py │ │ ├── generate_api_schema.py │ │ ├── graph_entity.py │ │ ├── graphql_mutation_from_summary.py │ │ ├── graphql_type_from_summary.py │ │ ├── parse_string.py │ │ ├── query_for_model.py │ │ ├── serialize_native_type.py │ │ ├── summarize_crud_mutation.py │ │ ├── summarize_mutation.py │ │ ├── summarize_mutation_io.py │ │ └── walk_query.py ├── auth │ ├── __init__.py │ ├── decorators.py │ ├── models │ │ ├── __init__.py │ │ ├── fields │ │ │ ├── __init__.py │ │ │ └── password.py │ │ ├── mixins │ │ │ ├── __init__.py │ │ │ ├── hasPassword.py │ │ │ └── user.py │ │ └── userPassword.py │ ├── primitives │ │ ├── __init__.py │ │ └── passwordHash.py │ └── util │ │ ├── __init__.py │ │ ├── generate_session_token.py │ │ ├── random_string.py │ │ ├── read_session_token.py │ │ └── token_encryption_algorithm.py ├── config │ ├── __init__.py │ └── config.py ├── contrib │ ├── __init__.py │ └── graphene_peewee │ │ ├── __init__.py │ │ ├── converter.py │ │ └── objectType.py ├── conventions │ ├── __init__.py │ ├── actions.py │ ├── api.py │ ├── auth.py │ ├── models.py │ └── services.py ├── database.py ├── management │ ├── __init__.py │ ├── scripts │ │ ├── __init__.py │ │ ├── create.py │ │ └── events │ │ │ ├── __init__.py │ │ │ ├── ask.py │ │ │ └── publish.py │ ├── templates │ │ ├── api │ │ │ └── {{name}} │ │ │ │ ├── README.md │ │ │ │ └── server.py │ │ ├── auth │ │ │ └── {{name}} │ │ │ │ ├── README.md │ │ │ │ └── server.py │ │ ├── common │ │ │ └── {{name}} │ │ │ │ ├── __init__.py │ │ │ │ └── manage.py │ │ ├── connection │ │ │ └── {{name}} │ │ │ │ ├── README.md │ │ │ │ └── server.py │ │ └── model │ │ │ └── {{name}} │ │ │ ├── README.md │ │ │ └── server.py │ └── util │ │ ├── __init__.py │ │ └── render_template.py ├── models │ ├── __init__.py │ ├── base.py │ ├── fields │ │ └── __init__.py │ ├── serializers │ │ ├── __init__.py │ │ └── modelSerializer.py │ └── util.py ├── network │ ├── __init__.py │ ├── events │ │ ├── __init__.py │ │ ├── actionHandlers │ │ │ ├── __init__.py │ │ │ ├── createHandler.py │ │ │ ├── crudHandler.py │ │ │ ├── deleteHandler.py │ │ │ ├── flexibleAPIHandler.py │ │ │ ├── queryHandler.py │ │ │ ├── readHandler.py │ │ │ ├── rollCallHandler.py │ │ │ └── updateHandler.py │ │ ├── consumers │ │ │ ├── __init__.py │ │ │ ├── actions.py │ │ │ ├── api.py │ │ │ └── kafka.py │ │ └── util.py │ └── http │ │ ├── __init__.py │ │ ├── requestHandler.py │ │ └── responses.py └── services │ ├── __init__.py │ ├── apiGateway.py │ ├── connectionService.py │ ├── modelService.py │ ├── service.py │ └── serviceManager.py ├── package.json ├── setup.cfg ├── setup.py ├── tasks.py └── tests ├── __init__.py ├── api ├── __init__.py ├── test_filter.py ├── test_schema.py └── test_util.py ├── auth ├── __init__.py ├── test_fields.py └── test_util.py ├── config ├── __init__.py └── test_config.py ├── contrib ├── __init__.py └── graphene_peewee │ ├── __init__.py │ ├── test_converter.py │ └── test_objecttype.py ├── conventions ├── __init__.py ├── test_action_types.py ├── test_api.py ├── test_auth.py ├── test_models.py └── test_services.py ├── management ├── __init__.py ├── mock_template │ ├── hello │ ├── subdir │ │ └── hello │ └── {{name}} ├── test_scripts.py └── test_util.py ├── models ├── __init__.py ├── test_baseModel.py ├── test_mixins.py ├── test_modelSerializer.py └── test_utils.py ├── network ├── __init__.py ├── test_action_handlers.py ├── test_http.py └── test_util.py ├── services ├── __init__.py ├── test_api_service.py ├── test_connection_service.py ├── test_model_service.py └── test_service.py └── util ├── __init__.py ├── decorators.py ├── mock.py ├── mock_connection_service.py ├── mock_model.py ├── mock_model_service.py └── tests └── test_mock.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | ./build/ 13 | docs/build 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Local database 39 | test.db 40 | nautilus.db 41 | 42 | .DS_Store 43 | node_modules 44 | .coverage 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5.0" 4 | # command to install dependencies 5 | install: 6 | - pip install . 7 | - pip install cov-core 8 | - pip install coveralls 9 | - pip install nose2 10 | # command to run tests 11 | script: 12 | - nose2 --with-coverage 13 | 14 | after_success: 15 | coveralls 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 Twitter, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include nautilus * 3 | global-exclude /__pycache__ 4 | global-exclude *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-over-kafka 2 | 3 | [![Build Status](https://travis-ci.org/nautilus/nautilus.svg?branch=master)](https://travis-ci.org/nautilus/nautilus) 4 | [![Coverage Status](https://coveralls.io/repos/github/nautilus/python/badge.svg?branch=master)](https://coveralls.io/github/nautilus/python?branch=master) 5 | 6 | This project is a framework for event-driven microservices. It attempts to provide extendible 7 | answers to common questions when building a moden web application so that you can focus 8 | on what you do best: building awesome, scalable services. Some of these features include: 9 | 10 | * Distributed authentication 11 | * Message passing 12 | * Couple-free service joins 13 | * Service API versioning (coming soon!) 14 | * A flexible GraphQL API that adapts as services come online 15 | * Distributed/remote database administration (coming soon!) 16 | 17 | Full documentation is hosted [here](http://alecaivazis.github.io/graphql-over-kafka/). 18 | 19 | ***NOTE***: At the moment, this project should be considered an experiment using kafka to perform service joins across 20 | very fine grain services. If you are interested in helping, please get in touch! 21 | 22 | ## Requirements 23 | * >= Python 3.5 24 | * Kafka 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | 2 | GH_PAGES_SOURCES = source sphinx.make ../nautilus 3 | 4 | all: local 5 | 6 | gh-pages: 7 | git checkout gh-pages ; \ 8 | git checkout master $(GH_PAGES_SOURCES) ; \ 9 | git reset HEAD ; \ 10 | make html -f ./sphinx.make ; \ 11 | rsync -av build/html/ .. ; \ 12 | rm -rf $(GH_PAGES_SOURCES) build ; \ 13 | git add .. ; \ 14 | git commit -m "Updated documentation" 15 | git push origin gh-pages; \ 16 | git checkout master 17 | 18 | local: clean 19 | make html -f ./sphinx.make 20 | 21 | clean: 22 | make clean -f ./sphinx.make 23 | 24 | build_internals: local 25 | 26 | publish: gh-pages 27 | 28 | -------------------------------------------------------------------------------- /docs/source/api/index.rst: -------------------------------------------------------------------------------- 1 | API Gateway 2 | ================ 3 | 4 | In order to reduce coupling between the individual services that make up 5 | your application, the api provided by an APIGateway service builds its 6 | graphql schema by waiting for services annouce their contributions 7 | when starting up and piecing together the complete summary of data 8 | availible from the backend services. 9 | 10 | 11 | .. autoclass:: nautilus.APIGateway 12 | :members: 13 | 14 | 15 | 16 | Filtering the API 17 | ------------------ 18 | 19 | The API for ``ModelService`` s (and other services based on them like ``ConnectionService``) have a few different filter arguments depending on the 20 | type of the attribute. The following filters show on both the API gateway as 21 | well as the inidividual service apis: 22 | 23 | +-----------+-------------------------+--------------------+-------------------------------------------------+ 24 | | Filter | Value type | Attribute type | Returns | 25 | +-----------+-------------------------+--------------------+-------------------------------------------------+ 26 | | | same as attribute | literal | all records with the matching attribute value | 27 | +-----------+-------------------------+--------------------+-------------------------------------------------+ 28 | | _in | list of attribute type | literal | all records with a value in the specified list | 29 | +-----------+-------------------------+--------------------+-------------------------------------------------+ 30 | | first | integer | any | return the first N records | 31 | +-----------+-------------------------+--------------------+-------------------------------------------------+ 32 | | last | integer | any | return the last N records | 33 | +-----------+-------------------------+--------------------+-------------------------------------------------+ 34 | | offset | integer | any | start the count filters at a given offset | 35 | +-----------+-------------------------+--------------------+-------------------------------------------------+ 36 | 37 | 38 | Designating a GraphQL Equivalent of a Nautilus Field 39 | ------------------------------------------------------- 40 | 41 | If you create (or find) a custom field that is compatible with peewee, the ORM 42 | used by nautilus internally, you might find the need to provide a custom handler 43 | in order for the schema generator to be able to convert it to the correct GraphQL 44 | type. 45 | 46 | 47 | .. autofunction:: nautilus.contrib.graphene_peewee.converter 48 | 49 | This function follows the [singledispatch] pattern. Registering a new type looks 50 | something like: 51 | 52 | .. code-block:: python 53 | 54 | from awesomepackage import AwesomeField 55 | from nautilus.contrib.graphene_peewee import converter 56 | 57 | @converter.register(AwesomeField) 58 | def convert_field_to_string(field): 59 | return String(description = field.doc) 60 | -------------------------------------------------------------------------------- /docs/source/architecture/event_based_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/event_based_layout.png -------------------------------------------------------------------------------- /docs/source/architecture/http_internal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/http_internal.png -------------------------------------------------------------------------------- /docs/source/architecture/http_related_cascade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/http_related_cascade.png -------------------------------------------------------------------------------- /docs/source/architecture/http_request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/http_request.png -------------------------------------------------------------------------------- /docs/source/architecture/http_single_cascade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/http_single_cascade.png -------------------------------------------------------------------------------- /docs/source/architecture/index.rst: -------------------------------------------------------------------------------- 1 | Architecture 2 | ============== 3 | 4 | Nautilus uses an event-based (also referred to as "event-sourced") architure 5 | that relies on a central messaging system through which all service communicate. 6 | This includes mutations of their various internal states as well as data-retrieval 7 | by the API gateway. 8 | 9 | 10 | What's Wrong With HTTP For Inter-Service Communication? 11 | -------------------------------------------------------- 12 | 13 | Nothing. However, When one tries to apply it to a large, distributed application 14 | (as is common today) a few issues start to surface concerning its directed 15 | semantics. To Illustrate this, imagine that we had two services behind our 16 | API gateway: 17 | 18 | .. image:: layout.png 19 | 20 | Now, if a client wants to send a request to the API in order to delete 21 | a particular user record, they would tradditionally send a ``DELETE`` (or the 22 | equivalent graphql query) to a specific url 23 | 24 | .. image:: http_request.png 25 | 26 | This request is then routed to a separate backend service which is responsible 27 | for handling the actual logic to be performed. 28 | 29 | .. image:: http_internal.png 30 | 31 | The question is - How do we tell the other service to remove entries 32 | corresponding to the user we just deleted? One way would be to have that 33 | service keep track of which services are connected to those user, and 34 | send a request to that service to remove them. 35 | 36 | .. image:: http_single_cascade.png 37 | 38 | This has a few issues. The first of which is that our services are now very 39 | tighly coupled with each other. If something were to change in the target 40 | service (say the format of record's unique id), without updating the user service, 41 | our system would no longer be able to perform the complete action. By "polluting" 42 | our service with assumptions of another, we force ourselves to slow down in order 43 | to ensure our changes aren't going to have un-forseen consequences. This is somewhat 44 | alleviated by the introduction of services that maintain a very small association 45 | between service records - similar to how a join table works in a relational database. 46 | 47 | .. image:: http_related_cascade.png 48 | 49 | However, this does not fix all of the issues associated with handling related data 50 | stored across many services. In order to pull this off, the user service needs to 51 | maintain a list of all related services so that it can make sure the related records 52 | are also removed. 53 | 54 | 55 | 56 | What Does Nautilus Do Differently? 57 | ----------------------------------- 58 | 59 | If you think about the problem presented, the core issues stems from the 60 | client/server paradigm of http communication. Because there can only be one 61 | recipient (the server), there can only be a single reponse to any given action. 62 | If you wanted to have many different services to react to the same event, one 63 | must imploy an event-system. While this idea is not new, Nautilus uses this 64 | line of thinking for not just asynchronous tasks but all inter-service communication, 65 | wether it is synchronous or not. 66 | 67 | To see how this looks, consider the situation from before: 68 | 69 | .. image:: event_based_layout.png 70 | 71 | Now, when the api recieves the request, rather than redirecting the request to the 72 | appropriate service, the API fires an event that will cause the intended behavior 73 | on the backend service. 74 | 75 | .. image:: kafka_api_event.png 76 | 77 | This action gets sent to all services however, the only one we are interested in 78 | at the moment is the user service so we will just draw that for now. 79 | 80 | .. image:: kafka_first_pending_simple.png 81 | 82 | After performing the specific action, the server responds with an event, indicating 83 | wether it was successful or not. After recieving this event, the API replies to the 84 | user, terminating the original request. 85 | 86 | .. image:: kafka_reply.png 87 | 88 | While this seems very similar to the interaction we looked at earlier, there is 89 | a key difference - by performing the synchronous communication over a globally 90 | accessible event system, other services can respond to the successfull event 91 | notifacation to "tidy up" the rest of the system, guaranteeing an 92 | eventually-consistent picture across all of our services. 93 | 94 | .. image:: kafka_final_cascade.png 95 | -------------------------------------------------------------------------------- /docs/source/architecture/kafka_api_event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/kafka_api_event.png -------------------------------------------------------------------------------- /docs/source/architecture/kafka_final_cascade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/kafka_final_cascade.png -------------------------------------------------------------------------------- /docs/source/architecture/kafka_first_pending_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/kafka_first_pending_simple.png -------------------------------------------------------------------------------- /docs/source/architecture/kafka_reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/kafka_reply.png -------------------------------------------------------------------------------- /docs/source/architecture/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/layout.png -------------------------------------------------------------------------------- /docs/source/architecture/service_join.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/docs/source/architecture/service_join.png -------------------------------------------------------------------------------- /docs/source/auth/index.rst: -------------------------------------------------------------------------------- 1 | Auth 2 | ===== 3 | 4 | This page summarizes points on authentication in nautilus apart from the basic 5 | workflow outlined in the `getting started guide <../intro/auth.html>`_. 6 | 7 | Customizing User Session 8 | ------------------------- 9 | 10 | Customizing the information stored in a user session (and therefore the criteria) 11 | by which you can authorize the user is easy. Simply define a method in your api 12 | gateway called ``user_session`` which takes the remote record of the user and 13 | returns a dictionary with that users sesstion: 14 | 15 | ..code-block:: python 16 | 17 | class MyAPI(nautilus.APIGateway): 18 | 19 | def user_session(user): 20 | return { 21 | 'id': user['id'], 22 | 'name': user['name'] 23 | } 24 | -------------------------------------------------------------------------------- /docs/source/handlers/index.rst: -------------------------------------------------------------------------------- 1 | Action Handlers 2 | ================ 3 | 4 | Action handlers describe how your service mutates its internal state in 5 | response to the arrival of an action from the queue. They are defined as 6 | a function of two arguments: ``action_type`` and ``payload``. ``Action_type`` 7 | is a string that classifies the event and ``payload`` is a dictionary 8 | representing the data associated with the event. For example, 9 | 10 | 11 | .. code-block:: python 12 | 13 | def action_handler(action_type, payload, properties): 14 | # if the payload represents a new recipe to add to the list 15 | if action_type == 'create_recipe': 16 | # create a new instance of the recipe model 17 | recipe = Recipe(**payload) 18 | # save the new model 19 | recipe.save() 20 | # otherwise if the payload is the id of a recipe to be deleted 21 | elif action_type == 'delete_recipe': 22 | # find the matching recipe 23 | recipe = Recipe.query.first(Recipe.id == payload) 24 | # remove the recipe from the database 25 | recipe.remove() 26 | 27 | 28 | Action Handlers are defined within the service: 29 | 30 | .. code-block:: python 31 | 32 | 33 | class MyActionHandler(nautilus.network.ActionHandler): 34 | async def handle_action(self, action_type, payload, props, **kwds): 35 | print("recieved action!") 36 | 37 | class MyService(Service): 38 | action_handler = MyActionHandler 39 | 40 | 41 | Reusing and Combining Action Handlers 42 | --------------------------------------- 43 | 44 | As your services get more complex, you'll want to split your action handler into 45 | separate functions which each get called with the given arguments. It can get tedious 46 | to pass the arguments to every function so Nautilus provides a function called 47 | ``combine_action_handlers`` which serves just this purpose: 48 | 49 | .. autofunction:: nautilus.network.events.combine_action_handlers 50 | 51 | .. code-block:: python 52 | 53 | from nautilus.network import combine_action_handlers 54 | 55 | def action_handler1(action_type, payload, properties): 56 | print("first handler fired!") 57 | 58 | def action_handler2(action_type, payload, properties): 59 | print("second handler fired!") 60 | 61 | combined_handler = combine_action_handlers( 62 | action_handler1, 63 | action_handler2 64 | ) 65 | 66 | Using it in an Action Handler looks something like: 67 | 68 | .. code-block:: python 69 | 70 | from nautilus.network import combine_action_handlers, ActionHandler 71 | 72 | class MyActionHandler(ActionHandler): 73 | 74 | async def handle_action(self, *args, **kwds): 75 | # assuming action_handlers 1 and 2 were defined as above 76 | combined = combine_action_handlers( 77 | action_handler1, 78 | action_handler2 79 | ) 80 | # call the combined handler 81 | combined(*args, **kwds) 82 | 83 | 84 | 85 | 86 | Provided Action Handlers 87 | ------------------------- 88 | 89 | Nautilus provides some action handlers to mix with your own services when creating 90 | custom solutions. 91 | 92 | Factories 93 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 94 | 95 | The following are functions that take a paramter and return an an action creator. 96 | 97 | .. autofunction:: nautilus.network.events.actionHandlers.crud_handler 98 | .. autofunction:: nautilus.network.events.actionHandlers.create_handler 99 | .. autofunction:: nautilus.network.events.actionHandlers.read_handler 100 | .. autofunction:: nautilus.network.events.actionHandlers.update_handler 101 | .. autofunction:: nautilus.network.events.actionHandlers.delete_handler 102 | .. autofunction:: nautilus.network.events.actionHandlers.roll_call_handler 103 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. nautilus documentation master file, created by 2 | sphinx-quickstart on Tue Feb 9 00:14:56 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. toctree:: 7 | :hidden: 8 | 9 | intro/index 10 | architecture/index 11 | services/index 12 | handlers/index 13 | api/index 14 | auth/index 15 | utilities/index 16 | internals/index 17 | 18 | Welcome to Nautilus! 19 | ===================== 20 | 21 | Nautilus is a framework for building secure, event-driven microservices and 22 | attempts to provide extendible implementations of common aspects of a moden 23 | web application so that you can focus on what you do best: building awesome, 24 | scalable services. Some of these features include: 25 | 26 | * Distributed authentication 27 | * Message passing 28 | * Couple-free service joins 29 | * Service API versioning (coming soon!) 30 | * A flexible GraphQL API that adapts as services come online 31 | * Distributed/remote database administration (coming soon!) 32 | 33 | 34 | At the moment, there is only a python implementation of the general architecture, 35 | but there are plans for more languages - stay tuned! 36 | -------------------------------------------------------------------------------- /docs/source/internals/index.rst: -------------------------------------------------------------------------------- 1 | Module Index 2 | ================ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.api 10 | nautilus.auth 11 | nautilus.config 12 | nautilus.contrib 13 | nautilus.conventions 14 | nautilus.management 15 | nautilus.models 16 | nautilus.network 17 | nautilus.services 18 | 19 | Submodules 20 | ---------- 21 | 22 | nautilus.database module 23 | ------------------------ 24 | 25 | .. automodule:: nautilus.database 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | 31 | Module contents 32 | --------------- 33 | 34 | .. automodule:: nautilus 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.api.endpoints.requestHandlers.rst: -------------------------------------------------------------------------------- 1 | nautilus.api.endpoints.requestHandlers package 2 | ============================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.api.endpoints.requestHandlers.apiQuery module 8 | ------------------------------------------------------ 9 | 10 | .. automodule:: nautilus.api.endpoints.requestHandlers.apiQuery 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nautilus.api.endpoints.requestHandlers.graphiql module 16 | ------------------------------------------------------ 17 | 18 | .. automodule:: nautilus.api.endpoints.requestHandlers.graphiql 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | nautilus.api.endpoints.requestHandlers.graphql module 24 | ----------------------------------------------------- 25 | 26 | .. automodule:: nautilus.api.endpoints.requestHandlers.graphql 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: nautilus.api.endpoints.requestHandlers 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.api.endpoints.rst: -------------------------------------------------------------------------------- 1 | nautilus.api.endpoints package 2 | ============================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.api.endpoints.requestHandlers 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: nautilus.api.endpoints 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.api.rst: -------------------------------------------------------------------------------- 1 | nautilus.api package 2 | ==================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.api.endpoints 10 | nautilus.api.util 11 | 12 | Submodules 13 | ---------- 14 | 15 | nautilus.api.filter module 16 | -------------------------- 17 | 18 | .. automodule:: nautilus.api.filter 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | nautilus.api.schema module 24 | -------------------------- 25 | 26 | .. automodule:: nautilus.api.schema 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: nautilus.api 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.api.util.rst: -------------------------------------------------------------------------------- 1 | nautilus.api.util package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.api.util.create_model_schema module 8 | -------------------------------------------- 9 | 10 | .. automodule:: nautilus.api.util.create_model_schema 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nautilus.api.util.fields_for_model module 16 | ----------------------------------------- 17 | 18 | .. automodule:: nautilus.api.util.fields_for_model 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | nautilus.api.util.generate_api_schema module 24 | -------------------------------------------- 25 | 26 | .. automodule:: nautilus.api.util.generate_api_schema 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | nautilus.api.util.graphql_type_from_summary module 32 | -------------------------------------------------- 33 | 34 | .. automodule:: nautilus.api.util.graphql_type_from_summary 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | nautilus.api.util.parse_string module 40 | ------------------------------------- 41 | 42 | .. automodule:: nautilus.api.util.parse_string 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | nautilus.api.util.walk_query module 48 | ----------------------------------- 49 | 50 | .. automodule:: nautilus.api.util.walk_query 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | 56 | Module contents 57 | --------------- 58 | 59 | .. automodule:: nautilus.api.util 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.auth.models.fields.rst: -------------------------------------------------------------------------------- 1 | nautilus.auth.models.fields package 2 | =================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.auth.models.fields.password module 8 | ------------------------------------------- 9 | 10 | .. automodule:: nautilus.auth.models.fields.password 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: nautilus.auth.models.fields 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.auth.models.mixins.rst: -------------------------------------------------------------------------------- 1 | nautilus.auth.models.mixins package 2 | =================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.auth.models.mixins.hasPassword module 8 | ---------------------------------------------- 9 | 10 | .. automodule:: nautilus.auth.models.mixins.hasPassword 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nautilus.auth.models.mixins.user module 16 | --------------------------------------- 17 | 18 | .. automodule:: nautilus.auth.models.mixins.user 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: nautilus.auth.models.mixins 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.auth.models.rst: -------------------------------------------------------------------------------- 1 | nautilus.auth.models package 2 | ============================ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.auth.models.fields 10 | nautilus.auth.models.mixins 11 | 12 | Submodules 13 | ---------- 14 | 15 | nautilus.auth.models.userPassword module 16 | ---------------------------------------- 17 | 18 | .. automodule:: nautilus.auth.models.userPassword 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: nautilus.auth.models 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.auth.primitives.rst: -------------------------------------------------------------------------------- 1 | nautilus.auth.primitives package 2 | ================================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.auth.primitives.passwordHash module 8 | -------------------------------------------- 9 | 10 | .. automodule:: nautilus.auth.primitives.passwordHash 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: nautilus.auth.primitives 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.auth.requestHandlers.forms.rst: -------------------------------------------------------------------------------- 1 | nautilus.auth.requestHandlers.forms package 2 | =========================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.auth.requestHandlers.forms.change module 8 | ------------------------------------------------- 9 | 10 | .. automodule:: nautilus.auth.requestHandlers.forms.change 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nautilus.auth.requestHandlers.forms.forgot module 16 | ------------------------------------------------- 17 | 18 | .. automodule:: nautilus.auth.requestHandlers.forms.forgot 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | nautilus.auth.requestHandlers.forms.login module 24 | ------------------------------------------------ 25 | 26 | .. automodule:: nautilus.auth.requestHandlers.forms.login 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | nautilus.auth.requestHandlers.forms.register module 32 | --------------------------------------------------- 33 | 34 | .. automodule:: nautilus.auth.requestHandlers.forms.register 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | Module contents 41 | --------------- 42 | 43 | .. automodule:: nautilus.auth.requestHandlers.forms 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.auth.requestHandlers.rst: -------------------------------------------------------------------------------- 1 | nautilus.auth.requestHandlers package 2 | ===================================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.auth.requestHandlers.forms 10 | 11 | Submodules 12 | ---------- 13 | 14 | nautilus.auth.requestHandlers.base module 15 | ----------------------------------------- 16 | 17 | .. automodule:: nautilus.auth.requestHandlers.base 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | nautilus.auth.requestHandlers.login module 23 | ------------------------------------------ 24 | 25 | .. automodule:: nautilus.auth.requestHandlers.login 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | nautilus.auth.requestHandlers.logout module 31 | ------------------------------------------- 32 | 33 | .. automodule:: nautilus.auth.requestHandlers.logout 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | nautilus.auth.requestHandlers.register module 39 | --------------------------------------------- 40 | 41 | .. automodule:: nautilus.auth.requestHandlers.register 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | 47 | Module contents 48 | --------------- 49 | 50 | .. automodule:: nautilus.auth.requestHandlers 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.auth.rst: -------------------------------------------------------------------------------- 1 | nautilus.auth package 2 | ===================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.auth.models 10 | nautilus.auth.primitives 11 | nautilus.auth.requestHandlers 12 | 13 | Submodules 14 | ---------- 15 | 16 | nautilus.auth.decorators module 17 | ------------------------------- 18 | 19 | .. automodule:: nautilus.auth.decorators 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | 25 | Module contents 26 | --------------- 27 | 28 | .. automodule:: nautilus.auth 29 | :members: 30 | :undoc-members: 31 | :show-inheritance: 32 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.config.rst: -------------------------------------------------------------------------------- 1 | nautilus.config package 2 | ======================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.config.config module 8 | ----------------------------- 9 | 10 | .. automodule:: nautilus.config.config 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: nautilus.config 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.contrib.graphene_peewee.rst: -------------------------------------------------------------------------------- 1 | nautilus.contrib.graphene_peewee package 2 | ======================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.contrib.graphene_peewee.converter module 8 | ------------------------------------------------- 9 | 10 | .. automodule:: nautilus.contrib.graphene_peewee.converter 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nautilus.contrib.graphene_peewee.objectType module 16 | -------------------------------------------------- 17 | 18 | .. automodule:: nautilus.contrib.graphene_peewee.objectType 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: nautilus.contrib.graphene_peewee 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.contrib.rst: -------------------------------------------------------------------------------- 1 | nautilus.contrib package 2 | ======================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.contrib.graphene_peewee 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: nautilus.contrib 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.conventions.rst: -------------------------------------------------------------------------------- 1 | nautilus.conventions package 2 | ============================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.conventions.actions module 8 | ----------------------------------- 9 | 10 | .. automodule:: nautilus.conventions.actions 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nautilus.conventions.api module 16 | ------------------------------- 17 | 18 | .. automodule:: nautilus.conventions.api 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | nautilus.conventions.auth module 24 | -------------------------------- 25 | 26 | .. automodule:: nautilus.conventions.auth 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | nautilus.conventions.models module 32 | ---------------------------------- 33 | 34 | .. automodule:: nautilus.conventions.models 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | nautilus.conventions.services module 40 | ------------------------------------ 41 | 42 | .. automodule:: nautilus.conventions.services 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | 48 | Module contents 49 | --------------- 50 | 51 | .. automodule:: nautilus.conventions 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.management.rst: -------------------------------------------------------------------------------- 1 | nautilus.management package 2 | =========================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.management.scripts 10 | nautilus.management.util 11 | 12 | Module contents 13 | --------------- 14 | 15 | .. automodule:: nautilus.management 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.management.scripts.rst: -------------------------------------------------------------------------------- 1 | nautilus.management.scripts package 2 | =================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.management.scripts.create module 8 | ----------------------------------------- 9 | 10 | .. automodule:: nautilus.management.scripts.create 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: nautilus.management.scripts 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.management.util.rst: -------------------------------------------------------------------------------- 1 | nautilus.management.util package 2 | ================================ 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: nautilus.management.util 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.models.fields.rst: -------------------------------------------------------------------------------- 1 | nautilus.models.fields package 2 | ============================== 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: nautilus.models.fields 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.models.rst: -------------------------------------------------------------------------------- 1 | nautilus.models package 2 | ======================= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.models.fields 10 | nautilus.models.serializers 11 | 12 | Submodules 13 | ---------- 14 | 15 | nautilus.models.base module 16 | --------------------------- 17 | 18 | .. automodule:: nautilus.models.base 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | nautilus.models.util module 24 | --------------------------- 25 | 26 | .. automodule:: nautilus.models.util 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: nautilus.models 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.models.serializers.rst: -------------------------------------------------------------------------------- 1 | nautilus.models.serializers package 2 | =================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.models.serializers.modelSerializer module 8 | -------------------------------------------------- 9 | 10 | .. automodule:: nautilus.models.serializers.modelSerializer 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: nautilus.models.serializers 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.network.events.actionHandlers.rst: -------------------------------------------------------------------------------- 1 | nautilus.network.events.actionHandlers package 2 | ============================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.network.events.actionHandlers.createHandler module 8 | ----------------------------------------------------------- 9 | 10 | .. automodule:: nautilus.network.events.actionHandlers.createHandler 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nautilus.network.events.actionHandlers.crudHandler module 16 | --------------------------------------------------------- 17 | 18 | .. automodule:: nautilus.network.events.actionHandlers.crudHandler 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | nautilus.network.events.actionHandlers.deleteHandler module 24 | ----------------------------------------------------------- 25 | 26 | .. automodule:: nautilus.network.events.actionHandlers.deleteHandler 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | nautilus.network.events.actionHandlers.readHandler module 32 | --------------------------------------------------------- 33 | 34 | .. automodule:: nautilus.network.events.actionHandlers.readHandler 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | nautilus.network.events.actionHandlers.rollCallHandler module 40 | ------------------------------------------------------------- 41 | 42 | .. automodule:: nautilus.network.events.actionHandlers.rollCallHandler 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | nautilus.network.events.actionHandlers.updateHandler module 48 | ----------------------------------------------------------- 49 | 50 | .. automodule:: nautilus.network.events.actionHandlers.updateHandler 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | 56 | Module contents 57 | --------------- 58 | 59 | .. automodule:: nautilus.network.events.actionHandlers 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.network.events.consumers.rst: -------------------------------------------------------------------------------- 1 | nautilus.network.events.consumers package 2 | ========================================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.network.events.consumers.actions module 8 | ------------------------------------------------ 9 | 10 | .. automodule:: nautilus.network.events.consumers.actions 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nautilus.network.events.consumers.api module 16 | -------------------------------------------- 17 | 18 | .. automodule:: nautilus.network.events.consumers.api 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | nautilus.network.events.consumers.kafka module 24 | ---------------------------------------------- 25 | 26 | .. automodule:: nautilus.network.events.consumers.kafka 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: nautilus.network.events.consumers 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.network.events.rst: -------------------------------------------------------------------------------- 1 | nautilus.network.events package 2 | =============================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.network.events.actionHandlers 10 | nautilus.network.events.consumers 11 | 12 | Submodules 13 | ---------- 14 | 15 | nautilus.network.events.util module 16 | ----------------------------------- 17 | 18 | .. automodule:: nautilus.network.events.util 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: nautilus.network.events 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.network.http.rst: -------------------------------------------------------------------------------- 1 | nautilus.network.http package 2 | ============================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.network.http.requestHandler module 8 | ------------------------------------------- 9 | 10 | .. automodule:: nautilus.network.http.requestHandler 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nautilus.network.http.respones module 16 | ------------------------------------- 17 | 18 | .. automodule:: nautilus.network.http.respones 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: nautilus.network.http 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.network.rst: -------------------------------------------------------------------------------- 1 | nautilus.network package 2 | ======================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | nautilus.network.events 10 | nautilus.network.http 11 | 12 | Module contents 13 | --------------- 14 | 15 | .. automodule:: nautilus.network 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/internals/nautilus.services.rst: -------------------------------------------------------------------------------- 1 | nautilus.services package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | nautilus.services.apiGateway module 8 | ----------------------------------- 9 | 10 | .. automodule:: nautilus.services.apiGateway 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nautilus.services.authService module 16 | ------------------------------------ 17 | 18 | .. automodule:: nautilus.services.authService 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | nautilus.services.connectionService module 24 | ------------------------------------------ 25 | 26 | .. automodule:: nautilus.services.connectionService 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | nautilus.services.modelService module 32 | ------------------------------------- 33 | 34 | .. automodule:: nautilus.services.modelService 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | nautilus.services.service module 40 | -------------------------------- 41 | 42 | .. automodule:: nautilus.services.service 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | nautilus.services.serviceManager module 48 | --------------------------------------- 49 | 50 | .. automodule:: nautilus.services.serviceManager 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | 56 | Module contents 57 | --------------- 58 | 59 | .. automodule:: nautilus.services 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | -------------------------------------------------------------------------------- /docs/source/intro/api.rst: -------------------------------------------------------------------------------- 1 | Querying The Distributed Structure 2 | =================================== 3 | 4 | We now have three different services which are each responsible for maintaining 5 | a small bit of the application. While this has many benefits, one rather large 6 | annoyance is the difficulty to create a summary of our application using data 7 | that spans many services. 8 | 9 | One way to solve this is to create a service known as an "API gateway" which, 10 | unlike the services we have encountered so far, does not maintain any sort of 11 | internal state. Instead, this service just has a single schema that describes 12 | the entire cloud. This has a few major benefits that are worth pointing out: 13 | 14 | * The distributed nature of the cloud is completely hidden behind a single endpoint 15 | * Authorization is maintained by a single service 16 | * Various network optimizations only need to occur in a single place 17 | * The list of external mutations is in one place 18 | 19 | 20 | Let's begin by creating a new file called ``api.py`` in our directory with the 21 | following contents: 22 | 23 | .. code-block :: python 24 | 25 | # external imports 26 | from nautilus import APIGateway, ServiceManager 27 | from nauitilus.auth.util import generate_secret_key 28 | 29 | class CatPhotoAPI(APIGateway): 30 | secret_key = generate_secret_key() 31 | 32 | manager = ServiceManager(service) 33 | 34 | if __name__ == '__main__': 35 | manager.run() 36 | 37 | When this service runs it sends an event that intstructs all other services to announce 38 | their api contributitions. This information is pieced together to form a single api endpoint 39 | that is used to query the entire system. With your CatPhoto service running, start up the api 40 | passing a different port: 41 | 42 | .. code-block :: bash 43 | 44 | python3 api.py runserver --port 8001 45 | 46 | And visit the `/graphiql` endpoint like before. You should be able to see your other services 47 | as queryable nodes in the api as well as some mutations to handle the internal 48 | state of our model services. 49 | 50 | Out of the box, nautilus provides a few filters for graphql nodes based on ModelServices, for 51 | more information visit the `APIGateway Documentation <../api/index.html#filtering-the-api>`_. 52 | -------------------------------------------------------------------------------- /docs/source/intro/auth.rst: -------------------------------------------------------------------------------- 1 | Authentication 2 | =============== 3 | 4 | Before we begin restricting bits of our api to particular users, we need a service to 5 | maintain user information (like their e-mails). So make a second file called ``user.py`` 6 | that resembles our other model services: 7 | 8 | .. code-block:: python 9 | 10 | import nautilus 11 | from nautilus.models import BaseModel, fields 12 | 13 | class User(BaseModel): 14 | email = fields.CharField() 15 | 16 | class ServiceConfig: 17 | database_url = 'sqlite:///user.db' 18 | 19 | class UserService(nautilus.ModelService): 20 | model = User 21 | config = ServiceConfig 22 | 23 | 24 | Registration and Logging Users In 25 | ----------------------------------- 26 | 27 | Along with queries and mutations corresponding to our remote services, the api 28 | gateway also provides a few mutations for validating user credentials and 29 | registering new users. In order to register a user, visit a running api gateway 30 | and send the following query: 31 | 32 | .. code-block:: text 33 | 34 | mutation { 35 | registerUser(email:"foo", password:"bar") { 36 | user { 37 | id 38 | } 39 | sessionToken 40 | } 41 | } 42 | 43 | Assuming the user information is valid, the gateway should reply with the requested 44 | information of the new user as well as a token that reqeusts that require authentication 45 | provide. 46 | 47 | The next time the user tries to access your application, they will probably need to 48 | provide their credentials a second time. In order to validate those, the api 49 | gateway provides another mutation for logging users in: 50 | 51 | .. code-block:: text 52 | 53 | mutation { 54 | loginUser(email:"foo", password:"bar") { 55 | user { 56 | id 57 | } 58 | sessionToken 59 | } 60 | } 61 | 62 | Make sure to copy that ``sessionToken`` down somewhere. We'll use it to authenticate our 63 | requests later on. 64 | 65 | 66 | Authorizing Users for Particular Pieces of Data 67 | ------------------------------------------------ 68 | 69 | Regardless of your application, not all bits of information are inteded for 70 | everyone to see. Eventually you'll want to be able to specify which users are 71 | able to see which entries in our api. Thankfully, hiding pieces of data based 72 | on the current user is easy in nautilus. Simply, add a function to the api 73 | decorated to match the service record. This function takes the user as an argument 74 | and returns true if the user can see the object and false if not. For example, say 75 | we had set up a relationship between recipes and users through another 76 | connection service with a relationship named "owner". We could limit the results 77 | in the recipe query to only those written by the current user by changing our api 78 | service to look something like: 79 | 80 | .. code-block:: python 81 | 82 | class API(nautilus.APIGateway): 83 | 84 | @nautilus.auth_criteria('catPhoto') 85 | async def auth_catPhoto(self, model, user_id): 86 | """ 87 | This function returns True if the given user is able to view 88 | this photo. 89 | """ 90 | return await model.owner._has_id(user_id) 91 | 92 | 93 | Providing Session Tokens to API Queries 94 | ---------------------------------------- 95 | 96 | In order to make an authenticated request to the API gateway, the request must contain 97 | the session token in the ``Authentication`` header as a ``Bearer`` roken. For example, 98 | say we logged in with a user and was given the session token ``foo``. Unfortunately, 99 | graphiql does not allow the user to specify specific headers on requests. In order 100 | to test authenticated route, we suggest you use a command utility like ``curl``: 101 | 102 | .. code-block:: bash 103 | 104 | curl --header "Authentication: Bearer foo" localhost:8000?query="{query { catPhoto { owner { id } } } }" 105 | -------------------------------------------------------------------------------- /docs/source/intro/connecting_services.rst: -------------------------------------------------------------------------------- 1 | Connecting Services Together 2 | ============================= 3 | 4 | Given their highly specialized nature, a service is not very useful on its 5 | own. So, let's add another to our example cloud. This is a good opportunity 6 | to use some of the services that nautilus provides which are great starting 7 | points when adding new functionalities to your cloud. 8 | 9 | 10 | A second service to connect to 11 | ------------------------------- 12 | 13 | Let's start by creating a new file called ``comments.py`` 14 | in our directory. Inside of this file, paste the following code: 15 | 16 | .. code-block:: python 17 | 18 | # third party imports 19 | from nautilus import ModelService, ServiceManager 20 | # third party imports 21 | from nautilus.models import BaseModel, fields 22 | 23 | 24 | class Comment(BaseModel): 25 | contents = fields.CharField() 26 | 27 | class ServiceConfig: 28 | database_url = 'sqlite:///comments.db' 29 | 30 | class CommentService(ModelService): 31 | model = Comment 32 | config = ServiceConfig 33 | 34 | 35 | manager = ServiceManager(CommentService) 36 | 37 | if __name__ == '__main__': 38 | manager.run() 39 | 40 | 41 | 42 | 43 | Connection Models 44 | ------------------- 45 | 46 | Another very common service in nautilus clouds is the ConnectionService which 47 | is a specialized ModelService whose internal data represents the relationship 48 | between two other services. If you are familiar with databases, connection 49 | services play the exact same role as join tables. 50 | 51 | If that made sense to you, feel free to skip this paragraph. To illustrate 52 | the role of connection services, say there is an entry in the recipe table 53 | with and id of 1 and a entry in the ingredient table which also has an id 54 | of 1. To express that the recipe contains a particular ingredient, we would 55 | add an entry in the connection service which has a value of 1 in the recipe 56 | column and a value of 1 in the ingredient column. Keep in mind that these 57 | columns do not necessarily have to be unique. If there was a second ingredient 58 | with id 2 that is also a part of the recipe, there would be a second entry in 59 | the connection service that has a 1 in the recipe column but this time there 60 | would be a 2 in the ingredient column. This relationship is called 61 | "many-to-many" because a recipe can have many ingredients and an ingredient can 62 | be a member of many recipes (neither column is unique). Relationships can also be 63 | classified as "one-to-one" and "one-to-many". 64 | 65 | Make a new file called ``comments.py`` next to the previously created 66 | files. Now, create a ConnectionService to manage the relationship between 67 | recipes and ingredients: 68 | 69 | .. code-block:: python 70 | 71 | # external imports 72 | from nautilus import ConnectionService 73 | 74 | class ServiceConfig: 75 | database_url = 'sqlite:///commentConnections.db' 76 | 77 | class Comments(ConnectionService): 78 | from_service = ('CatPhoto',) 79 | to_service = ('Comment',) 80 | 81 | config = ServiceConfig 82 | 83 | 84 | Create the database for the two services and add some more dummy entries. 85 | Make sure the two id's entered into the connection database correspond to actual 86 | entries in the appropriate database. Again, you can run the service and check out 87 | the various endpoints. 88 | 89 | -------------------------------------------------------------------------------- /docs/source/intro/first_model.rst: -------------------------------------------------------------------------------- 1 | Keeping Track of Data 2 | ====================== 3 | 4 | Handling persistent data is a common task of any web application, wether 5 | it's user's passwords or their horde of cat photos. In a nautilus application, 6 | each piece of data is maintained by a sepecialized service known as a "model 7 | service". The definition of a model service begins with the specification of 8 | the underlying model. Like services, this is done by defining a python class. 9 | Create a file called catPhoto.py and paste the following code: 10 | 11 | 12 | .. code-block:: python 13 | 14 | from nautilus import models 15 | from nautilus.models import fields 16 | 17 | class CatPhoto(models.BaseModel): 18 | photo_location = fields.CharField() 19 | 20 | 21 | This defines the underlying database table for our service. Once the model is 22 | defined, creating a service to manage the data is relatively easy: 23 | 24 | 25 | .. code-block:: python 26 | 27 | import nautilus 28 | from nautilus import models 29 | from nautilus.models import fields 30 | 31 | 32 | class CatPhoto(models.BaseModel): 33 | photo_location = fields.CharField() 34 | 35 | 36 | class ServiceConfig: 37 | database_url = 'sqlite:///catPhotos.db' 38 | 39 | class CatPhotoService(nautilus.ModelService): 40 | model = CatPhoto 41 | config = ServiceConfig 42 | 43 | 44 | Before we can interact with the service we just made, let's create a local database 45 | so we can store our records somewhere: 46 | 47 | 48 | .. code-block:: bash 49 | 50 | $ python3 ./models.py syncdb 51 | 52 | 53 | Querying for data 54 | ------------------ 55 | 56 | You should now see a file called ``nautilus.db`` in your directory. Run the model service 57 | by executing ``python3 ./catPhoto.py runserver`` and navigate to 58 | ``http://localhost:8000/graphiql`` (notice the i). 59 | 60 | Model services automatically generate a graphql (no i) api for the model. 61 | Graphiql (with i) is a standard user interface for interacting with these sorts 62 | APIs. For more information on graphql visit 63 | `here `_. 64 | Try creating a cat photo record with a ``photo_location`` of "foo" in the 65 | database either via the ``sqlite`` cli or an app like `SQLiteBrowser `_. 66 | 67 | Once you've done that, try executing the following query by entering it in the left panel 68 | of `graphiql`: 69 | 70 | .. code-block:: graphql 71 | 72 | { 73 | all_models { 74 | photo_location 75 | } 76 | } 77 | 78 | You should see a json reply that looks like: 79 | 80 | .. code-block:: json 81 | 82 | { 83 | "data": [{ 84 | "photo_location": "foo" 85 | }], 86 | "errors": [] 87 | } 88 | 89 | It's useful to know that you can have graphiql provide an auto-complete by pressing 90 | ``alt+space`` allowing you to discover availble queries, fields, and mutations 91 | without looking at a single line of source code! Give it a try. 92 | 93 | In the next section you'll learn about how Nautilus helps you join data across 94 | services to create very powerful applications out of highly reusable parts. 95 | 96 | -------------------------------------------------------------------------------- /docs/source/intro/first_service.rst: -------------------------------------------------------------------------------- 1 | Your First Service 2 | =================== 3 | 4 | Services are the fundamental building blocks for cloud applications powered by 5 | nautilus. Let's begin by creating a directory with an empty file somewhere on 6 | your computer for us to use as a playground. 7 | 8 | .. code-block:: bash 9 | 10 | $ mkdir nautilus_playground && \ 11 | cd nautilus_playground && \ 12 | touch server.py 13 | 14 | Now that we have a file, let's make our first service. Keep in mind, that 15 | this section is meant to illustrate the various parts of a service, as you 16 | will see in the next section, the service we are about to construct can be 17 | much more succintly created using one of nautilus's pre-packaged services. 18 | Open server.py in your favorite text editor and copy and paste the following: 19 | 20 | .. code-block:: python 21 | 22 | from nautilus import Service 23 | 24 | class RecipeService(Service): pass 25 | 26 | if __name__ == '__main__': 27 | # create an instance of the service 28 | service = RecipeService() 29 | 30 | # create a manager for the service 31 | manager = ServiceManager(RecipeService) 32 | 33 | if __name__ == '__main__': 34 | manager.run() 35 | 36 | 37 | Test that this works by executing this script in your console: 38 | 39 | .. code-block:: bash 40 | 41 | $ python3 ./server.py runserver 42 | 43 | 44 | If it complains about permissions, try running ``sudo chmod u+x ./server.py``. 45 | 46 | 47 | Right now, our service is nothing more than a python process. It doesn't react 48 | to the outside world, it doesn't store any data, nor does it provide 49 | anything for another service to use - let's change that. 50 | 51 | Responding to Actions 52 | ----------------------- 53 | 54 | Now that we have a service, we can start adding some functionality to it. In the 55 | nautilus architecture, services are triggered to perform certain actions by 56 | events. We describe how the service responds to those events through 57 | the ``Action Handler``, a class record that holds the event configuration 58 | as well as its behavior: 59 | 60 | 61 | .. code-block:: python 62 | 63 | from nautilus.network import ActionHandler 64 | 65 | class PrintHandler(ActionHandler): 66 | 67 | async def handle_action(self, action_type, payload, props): 68 | print('hello world!') 69 | 70 | The primary method of an ActionHandler takes three arguments: ``action_type``, 71 | ``payload``, and ``props``. ``Action_type`` identifies the event and 72 | ``payload`` provides the associated data. Ignore ``props`` for now. 73 | 74 | Passing the Action handler to the service takes a single line: 75 | 76 | .. code-block:: python 77 | 78 | from nautilus import Service, ServiceManager 79 | from nautilus.network import ActionHandler 80 | 81 | 82 | class PrintHandler(ActionHandler): 83 | 84 | async def handle_action(self, action_type, payload, props): 85 | print(action_type, payload) 86 | 87 | 88 | class MyService(Service): 89 | action_handler = PrintHandler 90 | 91 | 92 | manager = ServiceManager(RecipeService) 93 | 94 | if __name__ == '__main__': 95 | manager.run() 96 | 97 | 98 | Let's test your service using the command line interface provided by nautilus. 99 | Open up a new terminal and execute: 100 | 101 | .. code-block:: bash 102 | 103 | $ naut publish -p "hello world" 104 | 105 | You should see the message in your running service's console. This pattern 106 | can be made to acommodate most situations. For example, if you had 107 | some special behavior that you wanted your service to do (like send an email), 108 | you would triger that behavior by firing a "send_email" action type and 109 | responding appropriately: 110 | 111 | .. code-block:: python 112 | 113 | from nautilus.network import ActionHandler 114 | 115 | class EmailActionHandler(ActionHandler): 116 | 117 | async def handle_action(self, action_type, payload, props): 118 | 119 | if action_type == 'send_email': 120 | # send the body of the action as the email 121 | send_email(payload) 122 | 123 | 124 | 125 | Congratulations! You have finally pieced together a complete nautilus service. 126 | In the next section you will learn how to create services that 127 | manage and persist database entries for your application. -------------------------------------------------------------------------------- /docs/source/intro/index.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | ================ 3 | 4 | .. toctree:: 5 | 6 | installation 7 | first_service 8 | first_model 9 | connecting_services 10 | api 11 | auth 12 | -------------------------------------------------------------------------------- /docs/source/intro/installation.rst: -------------------------------------------------------------------------------- 1 | Installation / Setup 2 | ===================== 3 | 4 | On versions of Ubuntu greater than 16.0, you need to install a few system 5 | packages first: 6 | 7 | .. code-block:: bash 8 | 9 | $ sudo apt-get install libffi-dev libssl-dev 10 | 11 | 12 | Nautilus is availible using pip: 13 | 14 | .. code-block:: bash 15 | 16 | $ pip install nautilus 17 | 18 | Necessary Background Processes 19 | ------------------------------- 20 | 21 | In order to run a nautilus cloud, you must have kafka running on your local machine. For more information on 22 | kafka including how to run it locally, go `here `_. 23 | -------------------------------------------------------------------------------- /docs/source/services/index.rst: -------------------------------------------------------------------------------- 1 | Services 2 | ======== 3 | 4 | Services are the building block of nautilus clouds. Very simply, a 5 | nautilus service is a standalone process that responds to actions sent 6 | over the global event queue, maintains and mutates some internal state 7 | according to the action, and provides a summary of that internal state via 8 | a GraphQL API. 9 | 10 | Nautilus provides extendible implementations of common services as well as a base 11 | Service class which all act as good starting points for your own services: 12 | 13 | 14 | .. autoclass:: nautilus.ModelService 15 | :members: 16 | 17 | 18 | .. autoclass:: nautilus.ConnectionService 19 | :members: 20 | 21 | 22 | .. autoclass:: nautilus.APIGateway 23 | :members: 24 | 25 | 26 | .. autoclass:: nautilus.AuthService 27 | :members: 28 | 29 | 30 | .. autoclass:: nautilus.Service 31 | :members: 32 | -------------------------------------------------------------------------------- /docs/source/utilities/index.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | ========== 3 | -------------------------------------------------------------------------------- /example/api.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import nautilus 3 | from nautilus import APIGateway 4 | 5 | # create an api gateway with just the schema 6 | class RecipeBookAPIGateway(nautilus.APIGateway): 7 | 8 | @nautilus.auth_criteria('Ingredient') 9 | def auth_ingredient(self, model, user): 10 | # an ingredient can only be viewed by author 11 | return model.author == user 12 | 13 | # create a service manager to run the service 14 | manager = ServiceManager(RecipeBookAPIGateway) 15 | 16 | if __name__ == '__main__': 17 | manager.run() 18 | -------------------------------------------------------------------------------- /example/ingredients.py: -------------------------------------------------------------------------------- 1 | """ 2 | This service maintains the list of ingredients in our cloud. 3 | """ 4 | 5 | # third party imports 6 | from nautilus import ModelService 7 | # third party imports 8 | from nautilus.models import BaseModel, fields 9 | 10 | class Ingredient(BaseModel): 11 | name = fields.CharField() 12 | 13 | class ServiceConfig: 14 | database_url = 'sqlite:///ingredients.db' 15 | 16 | class IngredientService(ModelService): 17 | model = Ingredient, 18 | config = ServiceConfig 19 | 20 | # create a service manager to run the service 21 | manager = ServiceManager(IngredientService) 22 | 23 | if __name__ == '__main__': 24 | manager.run() 25 | -------------------------------------------------------------------------------- /example/recipeIngredients.py: -------------------------------------------------------------------------------- 1 | from nautilus import ConnectionService 2 | # import the services to connect 3 | from .ingredients import IngredientService 4 | from .recipes import RecipeService 5 | 6 | class ServiceConfig: 7 | database_url = 'sqlite:///ingredientRecipeConnections.db' 8 | 9 | class Ingredients(ConnectionService): 10 | to_serivce = ('Ingredient',) 11 | from_service = ('Recipe',) 12 | 13 | config = ServiceConfig 14 | 15 | # create a service manager to run the service 16 | manager = ServiceManager(Ingredients) 17 | 18 | if __name__ == '__main__': 19 | manager.run() 20 | -------------------------------------------------------------------------------- /example/recipes.py: -------------------------------------------------------------------------------- 1 | """ 2 | This service maintains the list of recipes in our cloud. 3 | """ 4 | 5 | # third party imports 6 | from nautilus import ModelService 7 | # third party imports 8 | from nautilus.models import BaseModel, fields 9 | 10 | class Recipe(BaseModel): 11 | name = fields.CharField() 12 | 13 | class ServiceConfig: 14 | database_url = 'sqlite:///recipes.db' 15 | 16 | class RecipeService(ModelService): 17 | model = Recipe 18 | config = ServiceConfig 19 | 20 | # create a service manager to run the service 21 | manager = ServiceManager(RecipeService) 22 | 23 | if __name__ == '__main__': 24 | manager.run() 25 | -------------------------------------------------------------------------------- /nautilus/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import Config 2 | from .services import * 3 | from .database import db 4 | from .auth.decorators import auth_criteria 5 | -------------------------------------------------------------------------------- /nautilus/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .schema import Schema 2 | -------------------------------------------------------------------------------- /nautilus/api/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import os 3 | # local imports 4 | from .requestHandlers.graphiql import GraphiQLRequestHandler 5 | from .requestHandlers.graphql import GraphQLRequestHandler 6 | 7 | root_dir = os.path.dirname(__file__) 8 | template_dir = os.path.join(root_dir, 'templates') 9 | static_dir = os.path.join(root_dir, 'static', 'build') 10 | -------------------------------------------------------------------------------- /nautilus/api/endpoints/requestHandlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/nautilus/api/endpoints/requestHandlers/__init__.py -------------------------------------------------------------------------------- /nautilus/api/endpoints/requestHandlers/apiQuery.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import json 3 | import graphql 4 | import functools 5 | # local imports 6 | import nautilus 7 | from nautilus.config import Config 8 | from nautilus.network.http import Response 9 | from nautilus.api.util import parse_string 10 | from .graphql import GraphQLRequestHandler 11 | 12 | 13 | class APIQueryHandler(GraphQLRequestHandler): 14 | """ 15 | The api query handler parses and executes the query by hand, 16 | requesting the appropriate data over the action system. Queries 17 | are validated using the internally tracked schema maintained by 18 | the service. 19 | """ 20 | 21 | async def _handle_query(self, query): 22 | 23 | 24 | # if there hasn't been a schema generated yet 25 | if not self.schema: 26 | # yell loudly 27 | result = json.dumps({ 28 | 'data': {}, 29 | 'errors': ['No schema for this service.'] 30 | }) 31 | # send the result of the introspection to the user 32 | return Response(body=result.encode()) 33 | 34 | try: 35 | # figure out if the query is an introspection 36 | is_introspection = graphql.parse(query).definitions[0].name.value == 'IntrospectionQuery' 37 | # if something went wrong 38 | except AttributeError: 39 | # its not 40 | is_introspection = False 41 | 42 | # if the query is an introspection 43 | if is_introspection: 44 | # handle it using the schema 45 | introspection = self.service.schema.execute(query) 46 | result = json.dumps({ 47 | 'data': {key: value for key,value in introspection.data.items()}, 48 | 'errors': introspection.errors 49 | }) 50 | # send the result of the introspection to the user 51 | return Response(body=result.encode()) 52 | 53 | # by default there is no current user 54 | current_user = None 55 | 56 | # if there is an authorization header 57 | if 'Authorization' in self.request.headers: 58 | # the authorization header value 59 | auth_header = self.request.headers['Authorization'] 60 | # the name of the token method 61 | method = 'Bearer' 62 | # only accept bearer tokens 63 | if method in auth_header: 64 | # pull the session token out from the header value 65 | session_token = auth_header.replace(method, '').strip() 66 | # create a config object from the current user session 67 | current_user = Config(self.service._read_session_token(session_token)) 68 | 69 | # otherwise its a normal query/mutation so walk it like normal 70 | response = await parse_string( 71 | query, 72 | self.service.object_resolver, 73 | self.service.connection_resolver, 74 | self.service.mutation_resolver, 75 | extra_mutations={ 76 | 'loginUser': self.service.login_user, 77 | 'registerUser': self.service.register_user 78 | }, 79 | current_user=current_user 80 | ) 81 | 82 | # pass the result to the request 83 | return Response(body=json.dumps(response).encode()) 84 | -------------------------------------------------------------------------------- /nautilus/api/endpoints/requestHandlers/graphiql.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import aiohttp_jinja2 3 | # local imports 4 | from nautilus.network.http import RequestHandler 5 | 6 | 7 | class GraphiQLRequestHandler(RequestHandler): 8 | 9 | @aiohttp_jinja2.template('graphiql.html') 10 | async def get(self): 11 | # write the template to the client 12 | return {} 13 | -------------------------------------------------------------------------------- /nautilus/api/endpoints/requestHandlers/graphql.py: -------------------------------------------------------------------------------- 1 | import json 2 | from graphql.error import format_error as format_graphql_error 3 | # local imports 4 | from nautilus.network.http import Response, RequestHandler 5 | 6 | class GraphQLRequestHandler(RequestHandler): 7 | 8 | async def get(self): 9 | 10 | try: 11 | # grab the query from the request parameters 12 | query = self.request.GET['query'] 13 | # if the user forgot to specify a query 14 | except KeyError: 15 | # return a graphql response with the error 16 | return Response(body=json.dumps({ 17 | 'errors': ['No query given.'] 18 | }).encode()) 19 | 20 | # handle the query 21 | return await self._handle_query(query) 22 | 23 | async def post(self): 24 | try: # grab the query from the request parameters 25 | query = self.request.POST['query'] 26 | # if the user forgot to specify a query 27 | except KeyError: 28 | # return a graphql response with the error 29 | return Response(body=json.dumps({ 30 | 'errors': ['No query given.'] 31 | }).encode()) 32 | 33 | # handle the query 34 | return await self._handle_query(query) 35 | 36 | 37 | @property 38 | def request_context(self): 39 | return self 40 | 41 | 42 | @property 43 | def schema(self): 44 | return self.service.schema 45 | 46 | 47 | @property 48 | def service(self): 49 | return self.__class__.service 50 | 51 | 52 | async def _handle_query(self, query): 53 | 54 | # log the request 55 | print("handling graphql query: {}".format(query)) 56 | 57 | # execute the query 58 | result = self.schema.execute( 59 | query, 60 | context_value=self.request_context 61 | ) 62 | 63 | # create a dictionary version of the result 64 | result_dict = dict( 65 | data=result.data, 66 | errors= [str(error) for error in result.errors] 67 | ) 68 | 69 | # send the response to the client and close its connection 70 | return Response(body=json.dumps(result_dict).encode()) 71 | -------------------------------------------------------------------------------- /nautilus/api/endpoints/static/src/scripts/graphiql.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import GraphiQL from 'graphiql'; 4 | import fetch from 'isomorphic-fetch'; 5 | 6 | function graphQLFetcher(graphQLParams) { 7 | return fetch(window.location.origin + '/?query='+graphQLParams['query'], { 8 | method: 'get', 9 | }).then(response => response.json()); 10 | } 11 | 12 | ReactDOM.render(, document.body); 13 | -------------------------------------------------------------------------------- /nautilus/api/endpoints/templates/graphiql.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /nautilus/api/schema.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | from graphene import Schema 3 | 4 | class Schema(Schema): 5 | """ 6 | This class creates a graphql schema that resolves its fields using 7 | the natuilus event queue for asynchronous data retrieval. 8 | """ 9 | def __init__(self, executor=None, auto_camelcase=None, **kwds): 10 | super().__init__( 11 | auto_camelcase=False, 12 | **kwds 13 | ) 14 | -------------------------------------------------------------------------------- /nautilus/api/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .create_model_schema import create_model_schema 2 | from .fields_for_model import fields_for_model 3 | from .generate_api_schema import generate_api_schema 4 | from .graphql_type_from_summary import graphql_type_from_summary 5 | from .parse_string import parse_string 6 | from .summarize_crud_mutation import summarize_crud_mutation 7 | from .summarize_mutation import summarize_mutation 8 | from .graphql_mutation_from_summary import graphql_mutation_from_summary 9 | from .summarize_mutation_io import summarize_mutation_io 10 | from .convert_typestring_to_api_native import convert_typestring_to_api_native 11 | from .serialize_native_type import serialize_native_type 12 | from .graph_entity import GraphEntity 13 | from .query_for_model import query_for_model 14 | from .arg_string_from_dict import arg_string_from_dict -------------------------------------------------------------------------------- /nautilus/api/util/arg_string_from_dict.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def arg_string_from_dict(arg_dict, **kwds): 4 | """ 5 | This function takes a series of ditionaries and creates an argument 6 | string for a graphql query 7 | """ 8 | # the filters dictionary 9 | filters = { 10 | **arg_dict, 11 | **kwds, 12 | } 13 | # return the correctly formed string 14 | return ", ".join("{}: {}".format(key, json.dumps(value)) for key,value in filters.items()) -------------------------------------------------------------------------------- /nautilus/api/util/build_native_type_dictionary.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import graphene 3 | # local imports 4 | from .convert_typestring_to_api_native import convert_typestring_to_api_native 5 | from .graphql_type_from_summary import graphql_type_from_summary 6 | 7 | def build_native_type_dictionary(fields, respect_required=False, wrap_field=True, name=''): 8 | """ 9 | This function takes a list of type summaries and builds a dictionary 10 | with native representations of each entry. Useful for dynamically 11 | building native class records from summaries. 12 | """ 13 | # a place to start when building the input field attributes 14 | input_fields = {} 15 | # go over every input in the summary 16 | for field in fields: 17 | field_name = name + field['name'] 18 | field_type = field['type'] 19 | 20 | # if the type field is a string 21 | if isinstance(field_type, str): 22 | # compute the native api type for the field 23 | field_type = convert_typestring_to_api_native(field_type)( 24 | # required=respect_required and field['required'] 25 | ) 26 | # add an entry in the attributes 27 | input_fields[field['name']] = field_type 28 | 29 | # we could also be looking at a dictionary 30 | elif isinstance(field_type, dict): 31 | 32 | object_fields = field_type['fields'] 33 | 34 | # add the dictionary to the parent as a graphql object type 35 | input_fields[field['name']] = graphql_type_from_summary( 36 | summary={ 37 | 'name': field_name+"ArgType", 38 | 'fields': object_fields 39 | } 40 | ) 41 | 42 | # if we are supposed to wrap the object in a field 43 | if wrap_field: 44 | # then wrap the value we just added 45 | input_fields[field['name']] = graphene.Field(input_fields[field['name']]) 46 | 47 | 48 | # we're done 49 | return input_fields 50 | -------------------------------------------------------------------------------- /nautilus/api/util/convert_typestring_to_api_native.py: -------------------------------------------------------------------------------- 1 | def convert_typestring_to_api_native(typestring): 2 | """ 3 | This function converts the typestring representation of an api type to 4 | the appropriate graphql object. 5 | """ 6 | import graphene 7 | return getattr(graphene, typestring) -------------------------------------------------------------------------------- /nautilus/api/util/create_model_schema.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import graphene 3 | from graphene import Field, List 4 | # local imports 5 | from ..filter import filter_model, args_for_model 6 | from nautilus.contrib.graphene_peewee import PeeweeObjectType, convert_peewee_field 7 | 8 | 9 | def create_model_schema(target_model): 10 | """ This function creates a graphql schema that provides a single model """ 11 | 12 | from nautilus.database import db 13 | 14 | # create the schema instance 15 | schema = graphene.Schema(auto_camelcase=False) 16 | 17 | # grab the primary key from the model 18 | primary_key = target_model.primary_key() 19 | primary_key_type = convert_peewee_field(primary_key) 20 | 21 | # create a graphene object 22 | class ModelObjectType(PeeweeObjectType): 23 | class Meta: 24 | model = target_model 25 | 26 | pk = Field(primary_key_type, description="The primary key for this object.") 27 | 28 | @graphene.resolve_only_args 29 | def resolve_pk(self): 30 | return getattr(self, self.primary_key().name) 31 | 32 | 33 | class Query(graphene.ObjectType): 34 | """ the root level query """ 35 | all_models = List(ModelObjectType, args=args_for_model(target_model)) 36 | 37 | 38 | @graphene.resolve_only_args 39 | def resolve_all_models(self, **args): 40 | # filter the model query according to the arguments 41 | # print(filter_model(target_model, args)[0].__dict__) 42 | return filter_model(target_model, args) 43 | 44 | 45 | # add the query to the schema 46 | schema.query = Query 47 | 48 | return schema 49 | 50 | -------------------------------------------------------------------------------- /nautilus/api/util/fields_for_model.py: -------------------------------------------------------------------------------- 1 | # local imports 2 | from nautilus.contrib.graphene_peewee import convert_peewee_field 3 | 4 | def fields_for_model(model): 5 | """ 6 | This function returns the fields for a schema that matches the provided 7 | nautilus model. 8 | 9 | Args: 10 | model (nautilus.model.BaseModel): The model to base the field list on 11 | 12 | Returns: 13 | (dict): A mapping of field names to 14 | graphql types 15 | """ 16 | 17 | # the attribute arguments (no filters) 18 | args = {field.name.lower() : convert_peewee_field(field) \ 19 | for field in model.fields()} 20 | # use the field arguments, without the segments 21 | return args 22 | -------------------------------------------------------------------------------- /nautilus/api/util/generate_api_schema.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import graphene 3 | from graphene import ObjectType, Field, List 4 | # local imports 5 | from .graphql_type_from_summary import graphql_type_from_summary 6 | from .graphql_mutation_from_summary import graphql_mutation_from_summary 7 | 8 | def generate_api_schema(models, connections=[], mutations=[], **schema_args): 9 | 10 | # collect the schema types 11 | schema_types = [] 12 | 13 | # for each model 14 | for model in models: 15 | # find any matching connections 16 | model_connections = [connection for connection in connections \ 17 | if connection['connection']['from']['service'] == model['name']] 18 | # build a graphql type for the model 19 | graphql_type = graphql_type_from_summary(model, model_connections) 20 | 21 | # add the graphql type to the list 22 | schema_types.append(graphql_type) 23 | 24 | # if there are types for the schema 25 | if schema_types: 26 | # create a query with a connection to each model 27 | query = type('Query', (ObjectType,), { 28 | field.__name__: List(field) for field in schema_types 29 | }) 30 | 31 | # create mutations for each provided mutation 32 | mutations = [graphql_mutation_from_summary(mut) for mut in mutations] 33 | 34 | # if there are mutations to add 35 | if mutations: 36 | # create an object type to contain the mutations 37 | mutations = type('Mutations', (ObjectType,), { 38 | mut._meta.object_name: graphene.Field(mut) for mut in mutations 39 | }) 40 | 41 | # build the schema with the query object 42 | schema = graphene.Schema( 43 | query=query, 44 | mutation=mutations, 45 | **schema_args 46 | ) 47 | 48 | return schema 49 | -------------------------------------------------------------------------------- /nautilus/api/util/graphql_mutation_from_summary.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import graphene 3 | # local imports 4 | from .build_native_type_dictionary import build_native_type_dictionary 5 | 6 | def graphql_mutation_from_summary(summary): 7 | """ 8 | This function returns a graphql mutation corresponding to the provided 9 | summary. 10 | """ 11 | # get the name of the mutation from the summary 12 | mutation_name = summary['name'] 13 | 14 | # print(summary) 15 | 16 | # the treat the "type" string as a gra 17 | input_name = mutation_name + "Input" 18 | input_fields = build_native_type_dictionary(summary['inputs'], name=input_name, respect_required=True) 19 | 20 | # the inputs for the mutation are defined by a class record 21 | inputs = type('Input', (object,), input_fields) 22 | 23 | # the outputs for the mutation are attributes to the class record 24 | output_name = mutation_name + "Output" 25 | outputs = build_native_type_dictionary(summary['outputs'], name=output_name) 26 | 27 | # a no-op in order to satisfy the introspection query 28 | mutate = classmethod(lambda *_, **__ : 'hello') 29 | 30 | # create the appropriate mutation class record 31 | mutation = type(mutation_name, (graphene.Mutation,), { 32 | 'Input': inputs, 33 | 'mutate': mutate, 34 | **outputs 35 | }) 36 | 37 | # return the newly created mutation record 38 | return mutation -------------------------------------------------------------------------------- /nautilus/api/util/graphql_type_from_summary.py: -------------------------------------------------------------------------------- 1 | # third party imports 2 | import graphene 3 | 4 | def graphql_type_from_summary(summary, connections=[]): 5 | # the name of the type 6 | name = summary['name'] 7 | # the fields of the type 8 | fields = { 9 | field['name']: getattr(graphene, field['type'])() \ 10 | for field in summary['fields'] 11 | } 12 | # add the connections to the model 13 | # for connection in connec 14 | connections = { 15 | field['name']: graphene.List(field['connection']['to']['service']) \ 16 | for field in connections 17 | } 18 | # print(connections) 19 | # merge the two field dictionaries 20 | class_fields = { 21 | **fields, 22 | **connections 23 | } 24 | 25 | graphql_type = type(name, (graphene.ObjectType,), class_fields) 26 | graphql_type._service_name = name 27 | 28 | # create the class record for the model 29 | return graphql_type 30 | -------------------------------------------------------------------------------- /nautilus/api/util/query_for_model.py: -------------------------------------------------------------------------------- 1 | from .arg_string_from_dict import arg_string_from_dict 2 | 3 | def query_for_model(fields, **filters): 4 | # if there are filters 5 | if filters: 6 | # the string for the filters 7 | filter_string = "(%s)" % arg_string_from_dict(filters) 8 | else: 9 | filter_string = '' 10 | 11 | # the query for the requested data 12 | return "query { all_models%s { %s } }" % (filter_string, ', '.join(fields)) 13 | -------------------------------------------------------------------------------- /nautilus/api/util/serialize_native_type.py: -------------------------------------------------------------------------------- 1 | # local imports 2 | from nautilus.contrib.graphene_peewee import convert_peewee_field 3 | 4 | def serialize_native_type(native_type): 5 | """ 6 | This function serializes the native object type for summaries 7 | """ 8 | return type(convert_peewee_field(native_type)).__name__ -------------------------------------------------------------------------------- /nautilus/api/util/summarize_crud_mutation.py: -------------------------------------------------------------------------------- 1 | # local imports 2 | from nautilus.conventions.actions import get_crud_action 3 | from nautilus.conventions.models import get_model_string 4 | from .summarize_mutation import summarize_mutation 5 | from nautilus.conventions.api import ( 6 | create_mutation_inputs, 7 | update_mutation_inputs, 8 | delete_mutation_inputs, 9 | create_mutation_outputs, 10 | update_mutation_outputs, 11 | delete_mutation_outputs, 12 | crud_mutation_name, 13 | ) 14 | 15 | def summarize_crud_mutation(method, model, isAsync=False): 16 | """ 17 | This function provides the standard form for crud mutations. 18 | """ 19 | 20 | # create the approrpriate action type 21 | action_type = get_crud_action(method=method, model=model) 22 | # the name of the mutation 23 | name = crud_mutation_name(model=model, action=method) 24 | # a mapping of methods to input factories 25 | input_map = { 26 | 'create': create_mutation_inputs, 27 | 'update': update_mutation_inputs, 28 | 'delete': delete_mutation_inputs, 29 | } 30 | # a mappting of methods to output factories 31 | output_map = { 32 | 'create': create_mutation_outputs, 33 | 'update': update_mutation_outputs, 34 | 'delete': delete_mutation_outputs, 35 | } 36 | # the inputs for the mutation 37 | inputs = input_map[method](model) 38 | # the mutation outputs 39 | outputs = output_map[method](model) 40 | 41 | # return the appropriate summary 42 | return summarize_mutation( 43 | mutation_name=name, 44 | event=action_type, 45 | isAsync=isAsync, 46 | inputs=inputs, 47 | outputs=outputs 48 | ) 49 | -------------------------------------------------------------------------------- /nautilus/api/util/summarize_mutation.py: -------------------------------------------------------------------------------- 1 | def summarize_mutation(mutation_name, event, inputs, outputs, isAsync=False): 2 | """ 3 | This function provides a standard representation of mutations to be 4 | used when services announce themselves 5 | """ 6 | return dict( 7 | name=mutation_name, 8 | event=event, 9 | isAsync=isAsync, 10 | inputs=inputs, 11 | outputs=outputs, 12 | ) -------------------------------------------------------------------------------- /nautilus/api/util/summarize_mutation_io.py: -------------------------------------------------------------------------------- 1 | def summarize_mutation_io(name, type, required=False): 2 | """ 3 | This function returns the standard summary for mutations inputs 4 | and outputs 5 | """ 6 | return dict( 7 | name=name, 8 | type=type, 9 | required=required 10 | ) -------------------------------------------------------------------------------- /nautilus/api/util/walk_query.py: -------------------------------------------------------------------------------- 1 | async def walk_query(obj, object_resolver, connection_resolver, errors, current_user=None, __naut_name=None, obey_auth=True, **filters): 2 | """ 3 | This function traverses a query and collects the corresponding 4 | information in a dictionary. 5 | """ 6 | # if the object has no selection set 7 | if not hasattr(obj, 'selection_set'): 8 | # yell loudly 9 | raise ValueError("Can only resolve objects, not primitive types") 10 | 11 | # the name of the node 12 | node_name = __naut_name or obj.name.value if obj.name else obj.operation 13 | 14 | # the selected fields 15 | selection_set = obj.selection_set.selections 16 | 17 | def _build_arg_tree(arg): 18 | """ 19 | This function recursively builds the arguments for lists and single values 20 | """ 21 | # TODO: what about object arguments?? 22 | 23 | # if there is a single value 24 | if hasattr(arg, 'value'): 25 | # assign the value to the filter 26 | return arg.value 27 | # otherwise if there are multiple values for the argument 28 | elif hasattr(arg, 'values'): 29 | return [_build_arg_tree(node) for node in arg.values] 30 | 31 | # for each argument on this node 32 | for arg in obj.arguments: 33 | # add it to the query filters 34 | filters[arg.name.value] = _build_arg_tree(arg.value) 35 | 36 | # the fields we have to ask for 37 | fields = [field for field in selection_set if not field.selection_set] 38 | # the links between objects 39 | connections = [field for field in selection_set if field.selection_set] 40 | 41 | try: 42 | # resolve the model with the given fields 43 | models = await object_resolver(node_name, [field.name.value for field in fields], current_user=current_user, obey_auth=obey_auth, **filters) 44 | # if something went wrong resolving the object 45 | except Exception as e: 46 | # add the error as a string 47 | errors.append(e.__str__()) 48 | # stop here 49 | return None 50 | 51 | # add connections to each matching model 52 | for model in models: 53 | # if is an id for the model 54 | if 'pk' in model: 55 | # for each connection 56 | for connection in connections: 57 | # the name of the connection 58 | connection_name = connection.name.value 59 | # the target of the connection 60 | node = { 61 | 'name': node_name, 62 | 'pk': model['pk'] 63 | } 64 | 65 | try: 66 | # go through the connection 67 | connected_ids, next_target = await connection_resolver( 68 | connection_name, 69 | node, 70 | ) 71 | 72 | # if there are connections 73 | if connected_ids: 74 | # add the id filter to the list 75 | filters['pk_in'] = connected_ids 76 | 77 | # add the connection field 78 | value = await walk_query( 79 | connection, 80 | object_resolver, 81 | connection_resolver, 82 | errors, 83 | current_user=current_user, 84 | obey_auth=obey_auth, 85 | __naut_name=next_target, 86 | **filters 87 | ) 88 | # there were no connections 89 | else: 90 | value = [] 91 | # if something went wrong 92 | except Exception as e: 93 | # add the error as a string 94 | errors.append(e.__str__()) 95 | # stop here 96 | value = None 97 | 98 | # set the connection to the appropriate value 99 | model[connection_name] = value 100 | 101 | # return the list of matching models 102 | return models 103 | 104 | -------------------------------------------------------------------------------- /nautilus/auth/__init__.py: -------------------------------------------------------------------------------- 1 | # externals 2 | import os 3 | # local imports 4 | from .decorators import * 5 | from .primitives import * 6 | from .models import * 7 | from .util import * 8 | 9 | 10 | root_dir = os.path.dirname(__file__) 11 | template_dir = os.path.join(root_dir, 'requestHandlers', 'templates') -------------------------------------------------------------------------------- /nautilus/auth/decorators.py: -------------------------------------------------------------------------------- 1 | # internal imports 2 | from nautilus.conventions.auth import cookie_name # this fixes a circular reference...... 3 | 4 | def auth_criteria(service): 5 | """ 6 | This decorator marks the function as the auth specifacation for a 7 | particular service. 8 | 9 | Args: 10 | service (str): The service that the function authorizes 11 | """ 12 | def decorate(handler): 13 | # add the flag that marks this function for a service 14 | handler._service_auth = service 15 | 16 | # return the decorated function 17 | return handler 18 | 19 | return decorate -------------------------------------------------------------------------------- /nautilus/auth/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .mixins import * 2 | from .fields import * 3 | from .userPassword import UserPassword 4 | -------------------------------------------------------------------------------- /nautilus/auth/models/fields/__init__.py: -------------------------------------------------------------------------------- 1 | from .password import PasswordField 2 | -------------------------------------------------------------------------------- /nautilus/auth/models/fields/password.py: -------------------------------------------------------------------------------- 1 | # from: http://variable-scope.com/posts/storing-and-verifying-passwords-with-sqlalchemy 2 | 3 | # local imports 4 | from nautilus.models import Field 5 | from ...primitives import PasswordHash 6 | # from nautilus.api import convert_sqlalchemy_type 7 | 8 | class PasswordField(Field): 9 | """ 10 | This field allows for safe storage of passwords in a database while 11 | supporting easy validation. 12 | 13 | Args: 14 | rounds (int, default=12): The number of layers of encryption to be 15 | performed on the hash. This value is upgradeable and can be 16 | increased at any time. The next time the value is updated, it 17 | will be saved with the increased encryption. 18 | 19 | Example: 20 | TODO: add example of equality test 21 | 22 | """ 23 | db_field = 'varchar' 24 | 25 | def __init__(self, rounds=12, **kwds): 26 | self.rounds = rounds 27 | super().__init__(**kwds) 28 | 29 | 30 | def db_value(self, value): 31 | """ 32 | This function is responsible for converting the python value 33 | to something that the databse knows how to handle - namely a string. 34 | """ 35 | # make sure the given value is valid and then return the correspond hash 36 | return self._convert(value).hash 37 | 38 | 39 | def python_value(self, value): 40 | """ 41 | This function returns the python object corresponding to the data 42 | left in the database. 43 | 44 | Convert the hash to a PasswordHash, if it's non-NULL. 45 | """ 46 | if value is not None: 47 | return PasswordHash(value, rounds=self.rounds) 48 | 49 | 50 | def _convert(self, value): 51 | """ 52 | This function is responsible for the actual conversion from a 53 | native type to a PasswordHash object. 54 | 55 | PasswordHash instances or None values will return unchanged. 56 | Strings will be hashed and the resulting PasswordHash returned. 57 | Any other input will result in a TypeError. 58 | """ 59 | # if the value is already a password hash 60 | if isinstance(value, PasswordHash): 61 | # then don't do anything 62 | return value 63 | # or if the value is a string 64 | elif isinstance(value, str): 65 | # convert it to a password hash 66 | return PasswordHash.new(value, self.rounds) 67 | # otherwise the value is something we can't convert 68 | elif value is not None: 69 | # fail loudly 70 | raise TypeError( 71 | 'Cannot convert {} to a PasswordHash'.format(type(value))) 72 | 73 | 74 | # # Graphene Support 75 | 76 | # @convert_sqlalchemy_type.register(PasswordField) 77 | # def convert_column_to_string(type, column): 78 | # """ Make sure the password is never included in a schema. """ 79 | # raise Exception("Passwords cannot be included in a schema. Make sure to explcitly ignore any password fields in models.") 80 | -------------------------------------------------------------------------------- /nautilus/auth/models/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from .user import User 2 | from .hasPassword import HasPassword 3 | -------------------------------------------------------------------------------- /nautilus/auth/models/mixins/hasPassword.py: -------------------------------------------------------------------------------- 1 | # local imports 2 | from nautilus.models import BaseModel 3 | from ..fields import PasswordField 4 | 5 | class HasPassword(BaseModel): 6 | password = PasswordField() 7 | -------------------------------------------------------------------------------- /nautilus/auth/models/mixins/user.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file defines the models used by the user service. 3 | """ 4 | 5 | # local imports 6 | from nautilus.models import BaseModel 7 | from nautilus.models.fields import CharField 8 | 9 | class User(BaseModel): 10 | """ The user model used by """ 11 | firstname = CharField(null=True) 12 | lastname = CharField(null=True) 13 | email = CharField(null=False, unique=True) 14 | -------------------------------------------------------------------------------- /nautilus/auth/models/userPassword.py: -------------------------------------------------------------------------------- 1 | # local imports 2 | from nautilus.models import fields, BaseModel 3 | from .mixins import HasPassword 4 | 5 | class UserPassword(HasPassword, BaseModel): 6 | user = fields.CharField(unique=True) # points to a remote user entry 7 | -------------------------------------------------------------------------------- /nautilus/auth/primitives/__init__.py: -------------------------------------------------------------------------------- 1 | from .passwordHash import PasswordHash 2 | -------------------------------------------------------------------------------- /nautilus/auth/primitives/passwordHash.py: -------------------------------------------------------------------------------- 1 | # from: http://variable-scope.com/posts/storing-and-verifying-passwords-with-sqlalchemy 2 | 3 | import bcrypt 4 | 5 | class PasswordHash: 6 | """ This is a wrapper class over password hashes that abstracts equality """ 7 | 8 | def __init__(self, hash_, rounds=None): 9 | # make sure the hash is valid 10 | if len(hash_) != 60: 11 | raise ValueError('bcrypt hash should be 60 chars.') 12 | elif hash_.count('$'.encode('utf-8')) != 3: 13 | raise ValueError('bcrypt hash should have 3x "$".') 14 | 15 | # save the required instance variables 16 | self.hash = hash_ 17 | # figure out the current strength based on the saved hash 18 | self.rounds = int(str(self.hash).split('$')[2]) 19 | # the intended number of rounds (in case there is an upgrade) 20 | self.desired_rounds = rounds or self.rounds 21 | 22 | 23 | # this allows us to easily check if a candidate password matches the hash 24 | # using: hash == 'foo' 25 | def __eq__(self, candidate): 26 | """Hashes the candidate string and compares it to the stored hash.""" 27 | 28 | if isinstance(candidate, str): 29 | # convert it to a byte string 30 | candidate = candidate.encode('utf-8') 31 | 32 | # if the candidate matches the saved hash 33 | if self.hash == bcrypt.hashpw(candidate, self.hash): 34 | # if the computed number of rounds is less than the designated one 35 | if self.rounds < self.desired_rounds: 36 | # rehash the password 37 | self.rehash(candidate) 38 | 39 | return True 40 | # otherwise the password doesn't match 41 | else: 42 | return False 43 | 44 | 45 | def __repr__(self): 46 | """Simple object representation.""" 47 | return '<{}: {}>'.format(type(self).__name__, self.hash) 48 | 49 | 50 | @classmethod 51 | def new(cls, password, rounds): 52 | """Creates a PasswordHash from the given password.""" 53 | if isinstance(password, str): 54 | password = password.encode('utf-8') 55 | return cls(cls._new(password, rounds)) 56 | 57 | 58 | @classmethod 59 | def coerce(cls, key, value): 60 | """Ensure that loaded values are PasswordHashes.""" 61 | if isinstance(value, PasswordHash): 62 | return value 63 | return super(PasswordHash, cls).coerce(key, value) 64 | 65 | 66 | @staticmethod 67 | def _new(password, rounds): 68 | """ 69 | Returns a new bcrypt hash for the given password and rounds. 70 | note: Implemented to reduce repitition in `new` and `rehash`. 71 | """ 72 | return bcrypt.hashpw(password, bcrypt.gensalt(rounds)) 73 | 74 | 75 | def rehash(self, password): 76 | """Recreates the internal hash.""" 77 | self.hash = self._new(password, self.desired_rounds) 78 | self.rounds = self.desired_rounds 79 | -------------------------------------------------------------------------------- /nautilus/auth/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .random_string import random_string 2 | from .generate_session_token import generate_session_token 3 | from .read_session_token import read_session_token -------------------------------------------------------------------------------- /nautilus/auth/util/generate_session_token.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import jwt 3 | # local imports 4 | from .token_encryption_algorithm import token_encryption_algorithm 5 | 6 | def generate_session_token(secret_key, **payload): 7 | """ 8 | This function generates a session token signed by the secret key which 9 | can be used to extract the user credentials in a verifiable way. 10 | """ 11 | return jwt.encode(payload, secret_key, algorithm=token_encryption_algorithm()).decode('utf-8') -------------------------------------------------------------------------------- /nautilus/auth/util/random_string.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import random 3 | import string 4 | 5 | def random_string(length): 6 | """ 7 | This function generates a crytographically secure random string of alphanumeric 8 | characters of the appropriate length using the system random libraries. 9 | """ 10 | return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length)) 11 | -------------------------------------------------------------------------------- /nautilus/auth/util/read_session_token.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import jwt 3 | # local imports 4 | from .token_encryption_algorithm import token_encryption_algorithm 5 | 6 | def read_session_token(secret_key, token): 7 | """ 8 | This function verifies the token using the secret key and returns its 9 | contents. 10 | """ 11 | return jwt.decode(token.encode('utf-8'), secret_key, 12 | algorithms=[token_encryption_algorithm()] 13 | ) -------------------------------------------------------------------------------- /nautilus/auth/util/token_encryption_algorithm.py: -------------------------------------------------------------------------------- 1 | def token_encryption_algorithm(): 2 | return 'HS256' -------------------------------------------------------------------------------- /nautilus/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import Config 2 | -------------------------------------------------------------------------------- /nautilus/config/config.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | class Config(dict): 4 | """ 5 | This class creates a general api for configuration. 6 | """ 7 | 8 | def __init__(self, *args, **kwds): 9 | 10 | # start off with the given values 11 | values = kwds 12 | # for each argument passed 13 | for arg in args: 14 | # if the argument is a dictionary 15 | if isinstance(arg, dict): 16 | values.update(arg) 17 | # otherwise if the argument is a class record 18 | if isinstance(arg, type): 19 | values.update(self._from_type(arg)) 20 | 21 | # use the kwds as keys 22 | self.update(kwds) 23 | 24 | 25 | def __getattr__(self, attr): 26 | """ 27 | This method allows the retrieval of internal keys like attributes 28 | """ 29 | # access the dictionary for attributes 30 | return self[attr] 31 | 32 | 33 | def __setattr__(self, attr, value): 34 | """ 35 | This method allows the setting of internal keys like attributes 36 | """ 37 | # update the internal structure 38 | self[attr] = value 39 | 40 | 41 | def _from_type(self, config): 42 | """ 43 | This method converts a type into a dict. 44 | """ 45 | def is_user_attribute(attr): 46 | return ( 47 | not attr.startswith('__') and 48 | not isinstance(getattr(config, attr), collections.abc.Callable) 49 | ) 50 | 51 | return {attr: getattr(config, attr) for attr in dir(config) \ 52 | if is_user_attribute(attr)} 53 | -------------------------------------------------------------------------------- /nautilus/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/nautilus/contrib/__init__.py -------------------------------------------------------------------------------- /nautilus/contrib/graphene_peewee/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines various patches for graphene implemented for 3 | nautilus. 4 | """ 5 | from .converter import convert_peewee_field 6 | from .objectType import PeeweeObjectType 7 | -------------------------------------------------------------------------------- /nautilus/contrib/graphene_peewee/converter.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines various patches for peewee to easily interact with 3 | graphene. 4 | """ 5 | 6 | # external imports 7 | from functools import singledispatch 8 | from graphene.core.types.scalars import ( 9 | Boolean, 10 | Float, 11 | ID, 12 | Int, 13 | String, 14 | ) 15 | # local imports 16 | import nautilus.models.fields as fields 17 | 18 | @singledispatch 19 | def convert_peewee_field(field): 20 | """ 21 | This helper converts a peewee field type into the appropriate type 22 | for a graphql schema. 23 | """ 24 | raise ValueError( 25 | "Unable to convert peewee field %s " % field 26 | ) 27 | 28 | 29 | @convert_peewee_field.register(fields.CharField) 30 | @convert_peewee_field.register(fields.DateField) 31 | @convert_peewee_field.register(fields.DateTimeField) 32 | @convert_peewee_field.register(fields.FixedCharField) 33 | @convert_peewee_field.register(fields.TextField) 34 | @convert_peewee_field.register(fields.TimeField) 35 | @convert_peewee_field.register(fields.UUIDField) 36 | def convert_field_to_string(field): 37 | return String(description=field.help_text) 38 | 39 | 40 | @convert_peewee_field.register(fields.PrimaryKeyField) 41 | def convert_field_to_pk(field): 42 | return ID(description=field.help_text) 43 | 44 | 45 | @convert_peewee_field.register(fields.IntegerField) 46 | @convert_peewee_field.register(fields.BigIntegerField) 47 | def convert_field_to_int(field): 48 | return Int(description=field.help_text) 49 | 50 | 51 | @convert_peewee_field.register(fields.DecimalField) 52 | @convert_peewee_field.register(fields.DoubleField) 53 | @convert_peewee_field.register(fields.FloatField) 54 | def convert_field_to_float(field): 55 | return Float(description=field.help_text) 56 | 57 | 58 | @convert_peewee_field.register(fields.BooleanField) 59 | def convert_field_to_bool(field): 60 | return Boolean(description=field.help_text) 61 | -------------------------------------------------------------------------------- /nautilus/contrib/graphene_peewee/objectType.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | from graphene.core.classtypes.objecttype import ObjectType 3 | from graphene.core.classtypes.objecttype import ObjectTypeOptions 4 | # local imports 5 | from nautilus.contrib.graphene_peewee import convert_peewee_field 6 | 7 | VALID_ATTRS = ('model',) 8 | 9 | class PeeweeObjectTypeOptions(ObjectTypeOptions): 10 | 11 | def __init__(self, *args, **kwds): 12 | super().__init__(*args, **kwds) 13 | self.valid_attrs += VALID_ATTRS 14 | self.model = None 15 | 16 | def contribute_to_class(self, cls, name): 17 | # bubble up the chain 18 | super().contribute_to_class(cls, name) 19 | # add the model to the class record 20 | cls.model = self.model 21 | 22 | 23 | class PeeweeObjectTypeMeta(type(ObjectType)): 24 | 25 | options_class = PeeweeObjectTypeOptions 26 | 27 | def construct(self, *args, **kwds): 28 | # pass the model to the class record 29 | self.model = self._meta.model 30 | # return the full class record 31 | return super().construct(*args, **kwds) 32 | 33 | 34 | def __new__(cls, name, bases, attributes, **kwds): 35 | 36 | full_attr = {} 37 | 38 | try: 39 | # for each field in the table 40 | for field in attributes['Meta'].model.fields(): 41 | # the name of the field in the schema 42 | field_name = field.name[0].lower() + field.name[1:] 43 | # add an entry for the field we were passed 44 | full_attr[field_name] = convert_peewee_field(field) 45 | # if there is no meta type defined 46 | except KeyError: 47 | # keep going 48 | pass 49 | # if there is no model defined 50 | except AttributeError: 51 | # yell loudly 52 | raise ValueError("PeeweeObjectsTypes must have a model.") 53 | 54 | 55 | # merge the given attributes ontop of the dynamic ones 56 | full_attr.update(attributes) 57 | 58 | # create the nex class records 59 | return super().__new__(cls, name, bases, full_attr, **kwds) 60 | 61 | 62 | 63 | class PeeweeObjectType(ObjectType, metaclass=PeeweeObjectTypeMeta): 64 | """ 65 | This class provides support for generating graphql ObjectTypes 66 | based on peewee models 67 | """ 68 | -------------------------------------------------------------------------------- /nautilus/conventions/__init__.py: -------------------------------------------------------------------------------- 1 | from .actions import * 2 | from .models import * 3 | from .services import * 4 | from .api import * 5 | -------------------------------------------------------------------------------- /nautilus/conventions/actions.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is responsible for centralizing the action conventions used in nautilus. 3 | """ 4 | # external imports 5 | import json 6 | # local imports 7 | from .models import get_model_string 8 | 9 | 10 | def get_crud_action(method, model, status='pending', **kwds): 11 | return "%s.%s.%s" % (method, get_model_string(model), status) 12 | 13 | 14 | def change_action_status(action_type, new_status): 15 | """ 16 | This function changes the status of an action type. 17 | """ 18 | # replace the last bit of a dot separate string with the new_status 19 | return "%s.%s" % ('.'.join(action_type.split('.')[:-1]) , new_status) 20 | 21 | 22 | def roll_call_type(): 23 | return "roll_call" 24 | 25 | 26 | # TODO: check that it the args actually implement Serializable 27 | def serialize_action(action_type, payload, **extra_fields): 28 | """ 29 | This function returns the conventional form of the actions. 30 | """ 31 | action_dict = dict( 32 | action_type=action_type, 33 | payload=payload, 34 | **extra_fields 35 | ) 36 | # return a serializable version 37 | return json.dumps(action_dict) 38 | 39 | 40 | def hydrate_action(serialized): 41 | """ 42 | This function takes a serialized action and provides the primitive 43 | data structure. 44 | """ 45 | try: 46 | return json.loads(serialized) 47 | except: 48 | return { 49 | 'action_type': 'unknown', 50 | 'payload': str(serialized) 51 | } 52 | 53 | def query_action_type(): 54 | """ 55 | This action type corresponds to an api query performed over the event system 56 | """ 57 | return get_crud_action(model='api', method='query') 58 | 59 | 60 | def intialize_service_action(all_services=False, **kwds): 61 | # get the name of the service 62 | name = 'service' if not all_services else '*' 63 | # treat initialization like a crud action for services 64 | return get_crud_action('init', name, **kwds) 65 | 66 | 67 | def success_status(): 68 | return 'success' 69 | 70 | 71 | def error_status(): 72 | return 'error' 73 | 74 | 75 | def pending_status(): 76 | return 'pending' 77 | -------------------------------------------------------------------------------- /nautilus/conventions/auth.py: -------------------------------------------------------------------------------- 1 | def cookie_name(): 2 | return 'nautilus_jwt' -------------------------------------------------------------------------------- /nautilus/conventions/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is responsible for centralizing the model conventions used in nautilus. 3 | """ 4 | 5 | def normalize_string(string): 6 | return string[0].lower() + string[1:] 7 | 8 | def get_model_string(model): 9 | """ 10 | This function returns the conventional action designator for a given model. 11 | """ 12 | name = model if isinstance(model, str) else model.__name__ 13 | return normalize_string(name) 14 | -------------------------------------------------------------------------------- /nautilus/conventions/services.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is responsible for centralizing the service conventions used in nautilus. 3 | """ 4 | 5 | # local imports 6 | from .models import get_model_string, normalize_string 7 | 8 | def model_service_name(model): 9 | ''' the name of a service that manages a model ''' 10 | return get_model_string(model) 11 | 12 | 13 | def auth_service_name(): 14 | return "auth" 15 | 16 | 17 | def api_gateway_name(): 18 | ''' the name of the default api gateway ''' 19 | return "api" 20 | 21 | 22 | def connection_service_name(service, *args): 23 | ''' the name of a service that manages the connection between services ''' 24 | # if the service is a string 25 | if isinstance(service, str): 26 | return service 27 | 28 | return normalize_string(type(service).__name__) 29 | -------------------------------------------------------------------------------- /nautilus/database.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | from peewee import Proxy 3 | 4 | # create a placeholder database 5 | db = Proxy() 6 | 7 | def init_db(database_url): 8 | """ 9 | This function initializes the global database with the given url. 10 | """ 11 | # utility function to parse database urls 12 | from playhouse.db_url import connect 13 | # initialize the peewee database with the appropriate engine 14 | db.initialize(connect(database_url)) 15 | -------------------------------------------------------------------------------- /nautilus/management/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # external imports 4 | import click 5 | # local imports 6 | from .scripts import create, publish, ask 7 | 8 | @click.group() 9 | def cli(): 10 | """ 11 | A collection of functions for managing nautilus clouds. 12 | """ 13 | pass 14 | 15 | # add the various sub commands to the manager 16 | cli.add_command(create) 17 | cli.add_command(publish) 18 | cli.add_command(ask) 19 | -------------------------------------------------------------------------------- /nautilus/management/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | from .create import create 2 | from .events import ask, publish -------------------------------------------------------------------------------- /nautilus/management/scripts/create.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines the various create scripts availible to the cloud 3 | manager 4 | """ 5 | # external imports 6 | import click 7 | # local imports 8 | from ..util import render_template 9 | from nautilus.auth.util import random_string 10 | 11 | 12 | @click.group() 13 | def create(): 14 | """ A set of generators for common files and directory strctures. """ 15 | pass 16 | 17 | @click.command() 18 | @click.argument('model_names', nargs=-1) 19 | def model(model_names): 20 | """ 21 | Creates the example directory structure necessary for a model service. 22 | """ 23 | # for each model name we need to create 24 | for model_name in model_names: 25 | # the template context 26 | context = { 27 | 'name': model_name, 28 | } 29 | 30 | # render the model template 31 | render_template(template='common', context=context) 32 | render_template(template='model', context=context) 33 | 34 | 35 | @click.command() 36 | def api(): 37 | """ 38 | Create the folder/directories for an ApiGateway service. 39 | """ 40 | # the template context 41 | context = { 42 | 'name': 'api', 43 | 'secret_key': random_string(32) 44 | } 45 | 46 | render_template(template='common', context=context) 47 | render_template(template='api', context=context) 48 | 49 | 50 | @click.command() 51 | def auth(): 52 | """ 53 | Create the folder/directories for an Auth service. 54 | """ 55 | # the template context 56 | context = { 57 | 'name': 'auth', 58 | } 59 | 60 | render_template(template='common', context=context) 61 | render_template(template='auth', context=context) 62 | 63 | 64 | @click.command() 65 | @click.argument('model_connections', nargs=-1) 66 | def connection(model_connections): 67 | """ 68 | Creates the example directory structure necessary for a connection 69 | service. 70 | """ 71 | 72 | # for each connection group 73 | for connection_str in model_connections: 74 | 75 | # the services to connect 76 | services = connection_str.split(':') 77 | services.sort() 78 | 79 | service_name = ''.join([service.title() for service in services]) 80 | 81 | # the template context 82 | context = { 83 | # make sure the first letter is lowercase 84 | 'name': service_name[0].lower() + service_name[1:], 85 | 'services': services, 86 | } 87 | 88 | render_template(template='common', context=context) 89 | render_template(template='connection', context=context) 90 | 91 | # add the various sub commands 92 | create.add_command(api) 93 | create.add_command(connection) 94 | create.add_command(model) 95 | create.add_command(auth) 96 | -------------------------------------------------------------------------------- /nautilus/management/scripts/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .ask import ask 2 | from .publish import publish -------------------------------------------------------------------------------- /nautilus/management/scripts/events/ask.py: -------------------------------------------------------------------------------- 1 | """ 2 | This command publishes a message to the event system for 3 | debugging perposes. 4 | """ 5 | 6 | # external imports 7 | import click 8 | import asyncio 9 | # local imports 10 | from nautilus.network.events.consumers import ActionHandler 11 | 12 | @click.option('--type', '-t', default='cli', help="The action type of the action to publish.") 13 | @click.option('--payload', '-p', required=True, help="The payload of the message") 14 | @click.command() 15 | def ask(type, payload): 16 | """ 17 | Publish a message with the specified action_type and payload over the 18 | event system. Useful for debugging. 19 | """ 20 | async def _produce(): 21 | # notify the user that we were successful 22 | print("Dispatching action with type {}...".format(type)) 23 | # fire an action with the given values 24 | response = await producer.ask(action_type=type, payload=payload) 25 | # show the user the reply 26 | print(response) 27 | 28 | # create a producer 29 | producer = ActionHandler() 30 | # start the producer 31 | producer.start() 32 | 33 | # get the current event loop 34 | loop = asyncio.get_event_loop() 35 | 36 | # run the production sequence 37 | loop.run_until_complete(_produce()) 38 | 39 | # start the producer 40 | producer.stop() 41 | -------------------------------------------------------------------------------- /nautilus/management/scripts/events/publish.py: -------------------------------------------------------------------------------- 1 | """ 2 | This command publishes a message to the event system for 3 | debugging perposes. 4 | """ 5 | 6 | # external imports 7 | import click 8 | import asyncio 9 | # local imports 10 | from nautilus.network.events.consumers import ActionHandler 11 | 12 | @click.option('--type', '-t', default='cli', help="The action type of the action to publish.") 13 | @click.option('--payload', '-p', required=True, help="The payload of the message") 14 | @click.command() 15 | def publish(type, payload): 16 | """ 17 | Publish a message with the specified action_type and payload over the 18 | event system. Useful for debugging. 19 | """ 20 | async def _produce(): 21 | # fire an action with the given values 22 | await producer.send(action_type=type, payload=payload) 23 | # notify the user that we were successful 24 | print("Successfully dispatched action with type {}.".format(type)) 25 | 26 | # create a producer 27 | producer = ActionHandler() 28 | # start the producer 29 | producer.start() 30 | 31 | # get the current event loop 32 | loop = asyncio.get_event_loop() 33 | 34 | # run the production sequence 35 | loop.run_until_complete(_produce()) 36 | 37 | # start the producer 38 | producer.stop() 39 | -------------------------------------------------------------------------------- /nautilus/management/templates/api/{{name}}/README.md: -------------------------------------------------------------------------------- 1 | # API Gateway 2 | 3 | A service that wraps over the entire cloud to centralize access and authorization of internal data 4 | -------------------------------------------------------------------------------- /nautilus/management/templates/api/{{name}}/server.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import nautilus 3 | 4 | class {{name.title()}}Service(nautilus.APIGateway): 5 | 6 | # Warning: This key should not be shared anywhere. Be careful when 7 | # comittting this service to version control to move this value 8 | # to an environment variable 9 | secret_key = '{{secret_key}}' 10 | -------------------------------------------------------------------------------- /nautilus/management/templates/auth/{{name}}/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/nautilus/management/templates/auth/{{name}}/README.md -------------------------------------------------------------------------------- /nautilus/management/templates/auth/{{name}}/server.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import nautilus 3 | 4 | class {{name.title()}}Service(nautilus.AuthService): pass 5 | -------------------------------------------------------------------------------- /nautilus/management/templates/common/{{name}}/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import service -------------------------------------------------------------------------------- /nautilus/management/templates/common/{{name}}/manage.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # third party imports 4 | from nautilus import ServiceManager 5 | # local imports 6 | from server import {{name.title()}}Service 7 | 8 | # create a manager wrapping the service 9 | manager = ServiceManager({{name.title()}}Service) 10 | 11 | if __name__ == '__main__': 12 | manager.run() 13 | -------------------------------------------------------------------------------- /nautilus/management/templates/connection/{{name}}/README.md: -------------------------------------------------------------------------------- 1 | # {% for service in services %}{{service.title()}} {% endfor %}Connection 2 | 3 | A service that manages the relationship between {% for service in services %}{% if loop.last %} and {%endif%}{{service}}s{% if not loop.last and loop|length > 2 %},{%endif%}{% endfor %} 4 | -------------------------------------------------------------------------------- /nautilus/management/templates/connection/{{name}}/server.py: -------------------------------------------------------------------------------- 1 | # third party imports 2 | import nautilus 3 | from nautilus import ConnectionService 4 | # import the services to connect{% for service in services %} 5 | from ..{{service}} import {{service.title()}}Service{% endfor %} 6 | 7 | class ServiceConfig: 8 | database_url = 'sqlite:////tmp/{{name}}.db' 9 | 10 | class {{name.title()}}Service(nautilus.ConnectionService): 11 | services = [{% for service in services %}{{service}}_service,{% endfor %}] 12 | config = ServiceConfig 13 | -------------------------------------------------------------------------------- /nautilus/management/templates/model/{{name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{name.title()}} 2 | 3 | A service for managing {{name.title()}}s. 4 | -------------------------------------------------------------------------------- /nautilus/management/templates/model/{{name}}/server.py: -------------------------------------------------------------------------------- 1 | # third party imports 2 | import nautilus 3 | from nautilus.models import BaseModel, fields 4 | 5 | class {{name.title()}}(BaseModel): 6 | pass 7 | 8 | class ServiceConfig: 9 | database_url = 'sqlite:///{{name}}.db' 10 | 11 | class {{name.title()}}Service(nautilus.ModelService): 12 | model = {{name.title()}} 13 | config = ServiceConfig 14 | -------------------------------------------------------------------------------- /nautilus/management/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .render_template import render_template -------------------------------------------------------------------------------- /nautilus/management/util/render_template.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import os 3 | import errno 4 | from jinja2 import Template 5 | 6 | def render_template(template, out_dir='.', context=None): 7 | ''' 8 | This function renders the template desginated by the argument to the 9 | designated directory using the given context. 10 | 11 | Args: 12 | template (string) : the source template to use (relative to ./templates) 13 | out_dir (string) : the name of the output directory 14 | context (dict) : the template rendering context 15 | ''' 16 | # the directory containing templates 17 | template_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 18 | '..', 19 | 'templates', 20 | template 21 | ) 22 | 23 | # the files and empty directories to copy 24 | files = [] 25 | empty_dirs = [] 26 | 27 | for (dirpath, _, filenames) in os.walk(template_directory): 28 | # if there are no files in the directory 29 | if len(filenames) == 0: 30 | # add the directory to the list 31 | empty_dirs.append(os.path.relpath(dirpath, template_directory)) 32 | # otherwise there are files in this directory 33 | else: 34 | # add the files to the list 35 | files.extend([os.path.join(dirpath, filepath) for filepath in filenames]) 36 | 37 | # for each template file 38 | for source_file in files: 39 | # open a new file that we are going to write to 40 | with open(source_file, 'r') as file: 41 | # create a template out of the source file contents 42 | template = Template(file.read()) 43 | # render the template with the given contents 44 | template_rendered = template.render(**(context or {})) 45 | 46 | # the location of the source relative to the template directory 47 | source_relpath = os.path.relpath(source_file, template_directory) 48 | 49 | # the target filename 50 | filename = os.path.join(out_dir, source_relpath) 51 | # create a jinja template out of the file path 52 | filename_rendered = Template(filename).render(**context) 53 | 54 | # the directory of the target file 55 | source_dir = os.path.dirname(filename_rendered) 56 | # if the directory doesn't exist 57 | if not os.path.exists(source_dir): 58 | # create the directories 59 | os.makedirs(source_dir) 60 | 61 | # create the target file 62 | with open(filename_rendered, 'w') as target_file: 63 | # write the rendered template to the target file 64 | target_file.write(template_rendered) 65 | 66 | # for each empty directory 67 | for dirpath in empty_dirs: 68 | try: 69 | # dirname 70 | dirname = os.path.join(out_dir, dirpath) 71 | # treat the dirname as a jinja template 72 | dirname_rendered = Template(dirname).render(**context) 73 | 74 | # if the directory doesn't exist 75 | if not os.path.exists(dirname_rendered): 76 | # create the directory in the target, replacing the name 77 | os.makedirs(dirname_rendered) 78 | except OSError as exc: 79 | # if the directory already exists 80 | if exc.errno == errno.EEXIST and os.path.isdir(dirpath): 81 | # keep going (noop) 82 | pass 83 | # otherwise its an error we don't handle 84 | else: 85 | # pass it along 86 | raise 87 | -------------------------------------------------------------------------------- /nautilus/models/__init__.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | from peewee import Field 3 | # local imports 4 | from .base import BaseModel 5 | from .util import * 6 | from .serializers import * 7 | from .fields import * 8 | -------------------------------------------------------------------------------- /nautilus/models/base.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import peewee 3 | # local imports 4 | from ..database import db 5 | 6 | class _Meta(type): 7 | """ 8 | The base metaclass for the nautilus models. 9 | """ 10 | 11 | def __init__(self, name, bases, attributes): 12 | # create the super class 13 | super().__init__(name, bases, attributes) 14 | # for each base we inherit from 15 | for base in bases: 16 | # if the base defines some mixin behavior 17 | if hasattr(base, '__mixin__'): 18 | # treat the base like a mixin 19 | base.__mixin__(self) 20 | 21 | # save the name in the class 22 | self.model_name = name 23 | 24 | 25 | class _MixedMeta(_Meta, peewee.BaseModel): 26 | """ 27 | This meta class mixes the sqlalchemy model meta class and the nautilus one. 28 | """ 29 | 30 | class BaseModel(peewee.Model, metaclass=_MixedMeta): 31 | 32 | class Meta: 33 | database = db 34 | 35 | 36 | def _json(self): 37 | # build a dictionary out of just the columns in the table 38 | return { 39 | field.name: getattr(self, field.name) \ 40 | for field in type(self).fields() 41 | } 42 | 43 | 44 | @classmethod 45 | def primary_key(cls): 46 | """ 47 | Retrieve the primary key of the database table. 48 | """ 49 | return cls._meta.primary_key 50 | 51 | 52 | @classmethod 53 | def required_fields(cls): 54 | """ 55 | Retrieve the required fields for this model. 56 | """ 57 | return [field for field in cls.fields() if not field.null] 58 | 59 | 60 | @classmethod 61 | def fields(cls): 62 | """ 63 | Returns the fields of the table. 64 | """ 65 | return cls._meta.fields.values() 66 | -------------------------------------------------------------------------------- /nautilus/models/fields/__init__.py: -------------------------------------------------------------------------------- 1 | from peewee import ( 2 | BareField, 3 | BigIntegerField, 4 | BlobField, 5 | BooleanField, 6 | CharField, 7 | DateField, 8 | DateTimeField, 9 | DecimalField, 10 | DoubleField, 11 | FixedCharField, 12 | FloatField, 13 | ForeignKeyField, 14 | IntegerField, 15 | PrimaryKeyField, 16 | TextField, 17 | TimeField, 18 | UUIDField, 19 | ) 20 | -------------------------------------------------------------------------------- /nautilus/models/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | from .modelSerializer import ModelSerializer 2 | -------------------------------------------------------------------------------- /nautilus/models/serializers/modelSerializer.py: -------------------------------------------------------------------------------- 1 | from json import JSONEncoder 2 | 3 | class ModelSerializer(JSONEncoder): 4 | """ 5 | This encoder serializes nautilus models to JSON 6 | """ 7 | 8 | def default(self, obj): 9 | try: 10 | # use the custom json handler 11 | return obj._json() 12 | 13 | # if the custom json handler doesn't exist 14 | except AttributeError: 15 | # perform the normal behavior 16 | return JSONEncoder.default(self, obj) 17 | 18 | def serialize(self, obj): 19 | """ 20 | This function performs the serialization on the given object. 21 | """ 22 | return self.encode(obj) 23 | -------------------------------------------------------------------------------- /nautilus/models/util.py: -------------------------------------------------------------------------------- 1 | # local imports 2 | from nautilus.conventions.services import connection_service_name, model_service_name 3 | from nautilus.models import BaseModel, fields 4 | 5 | def create_connection_model(service): 6 | """ Create an SQL Alchemy table that connects the provides services """ 7 | # the services connected 8 | services = service._services 9 | 10 | # the mixins / base for the model 11 | bases = (BaseModel,) 12 | # the fields of the derived 13 | attributes = {model_service_name(service): fields.CharField() for service in services} 14 | 15 | # create an instance of base model with the right attributes 16 | return type(BaseModel)(connection_service_name(service), bases, attributes) 17 | -------------------------------------------------------------------------------- /nautilus/network/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .http import RequestHandler, Response -------------------------------------------------------------------------------- /nautilus/network/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .consumers import * 2 | from .actionHandlers import * 3 | from .util import * 4 | -------------------------------------------------------------------------------- /nautilus/network/events/actionHandlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .crudHandler import crud_handler 2 | from .createHandler import create_handler 3 | from .updateHandler import update_handler 4 | from .deleteHandler import delete_handler 5 | from .readHandler import read_handler 6 | from .rollCallHandler import roll_call_handler 7 | from .queryHandler import query_handler 8 | from .flexibleAPIHandler import flexible_api_handler 9 | 10 | async def noop_handler(action_type, payload, dispatcher=None): 11 | return 12 | -------------------------------------------------------------------------------- /nautilus/network/events/actionHandlers/createHandler.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import json 3 | # local imports 4 | from nautilus.conventions.actions import ( 5 | get_crud_action, 6 | change_action_status, 7 | success_status, 8 | error_status 9 | ) 10 | from nautilus.models.serializers import ModelSerializer 11 | 12 | def create_handler(Model, name=None, **kwds): 13 | """ 14 | This factory returns an action handler that creates a new instance of 15 | the specified model when a create action is recieved, assuming the 16 | action follows nautilus convetions. 17 | 18 | Args: 19 | Model (nautilus.BaseModel): The model to create when the action 20 | received. 21 | 22 | Returns: 23 | function(action_type, payload): The action handler for this model 24 | """ 25 | async def action_handler(service, action_type, payload, props, notify=True, **kwds): 26 | # if the payload represents a new instance of `Model` 27 | if action_type == get_crud_action('create', name or Model): 28 | # print('handling create for ' + name or Model) 29 | try: 30 | # the props of the message 31 | message_props = {} 32 | # if there was a correlation id in the request 33 | if 'correlation_id' in props: 34 | # make sure it ends up in the reply 35 | message_props['correlation_id'] = props['correlation_id'] 36 | 37 | # for each required field 38 | for requirement in Model.required_fields(): 39 | 40 | # save the name of the field 41 | field_name = requirement.name 42 | # ensure the value is in the payload 43 | # TODO: check all required fields rather than failing on the first 44 | if not field_name in payload and field_name != 'id': 45 | # yell loudly 46 | raise ValueError( 47 | "Required field not found in payload: %s" %field_name 48 | ) 49 | 50 | # create a new model 51 | new_model = Model(**payload) 52 | 53 | # save the new model instance 54 | new_model.save() 55 | 56 | # if we need to tell someone about what happened 57 | if notify: 58 | # publish the scucess event 59 | await service.event_broker.send( 60 | payload=ModelSerializer().serialize(new_model), 61 | action_type=change_action_status(action_type, success_status()), 62 | **message_props 63 | ) 64 | 65 | # if something goes wrong 66 | except Exception as err: 67 | # if we need to tell someone about what happened 68 | if notify: 69 | # publish the error as an event 70 | await service.event_broker.send( 71 | payload=str(err), 72 | action_type=change_action_status(action_type, error_status()), 73 | **message_props 74 | ) 75 | # otherwise we aren't supposed to notify 76 | else: 77 | # raise the exception normally 78 | raise err 79 | 80 | 81 | 82 | # return the handler 83 | return action_handler 84 | -------------------------------------------------------------------------------- /nautilus/network/events/actionHandlers/crudHandler.py: -------------------------------------------------------------------------------- 1 | def crud_handler(Model, name=None, **kwds): 2 | """ 3 | This action handler factory reaturns an action handler that 4 | responds to actions with CRUD types (following nautilus conventions) 5 | and performs the necessary mutation on the model's database. 6 | 7 | Args: 8 | Model (nautilus.BaseModel): The model to delete when the action 9 | received. 10 | 11 | Returns: 12 | function(type, payload): The action handler for this model 13 | """ 14 | 15 | # import the necessary modules 16 | from nautilus.network.events import combine_action_handlers 17 | from . import update_handler, create_handler, delete_handler, read_handler 18 | 19 | # combine them into one handler 20 | return combine_action_handlers( 21 | create_handler(Model, name=name), 22 | read_handler(Model, name=name), 23 | update_handler(Model, name=name), 24 | delete_handler(Model, name=name), 25 | ) 26 | -------------------------------------------------------------------------------- /nautilus/network/events/actionHandlers/deleteHandler.py: -------------------------------------------------------------------------------- 1 | # local imports 2 | from nautilus.conventions.actions import ( 3 | get_crud_action, 4 | change_action_status, 5 | success_status, 6 | error_status 7 | ) 8 | from nautilus.models.serializers import ModelSerializer 9 | 10 | def delete_handler(Model, name=None, **kwds): 11 | """ 12 | This factory returns an action handler that deletes a new instance of 13 | the specified model when a delete action is recieved, assuming the 14 | action follows nautilus convetions. 15 | 16 | Args: 17 | Model (nautilus.BaseModel): The model to delete when the action 18 | received. 19 | 20 | Returns: 21 | function(type, payload): The action handler for this model 22 | """ 23 | # necessary imports 24 | from nautilus.database import db 25 | 26 | async def action_handler(service, action_type, payload, props, notify=True, **kwds): 27 | # if the payload represents a new instance of `model` 28 | if action_type == get_crud_action('delete', name or Model): 29 | try: 30 | # the props of the message 31 | message_props = {} 32 | # if there was a correlation id in the request 33 | if 'correlation_id' in props: 34 | # make sure it ends up in the reply 35 | message_props['correlation_id'] = props['correlation_id'] 36 | # the id in the payload representing the record to delete 37 | record_id = payload['id'] if 'id' in payload else payload['pk'] 38 | # get the model matching the payload 39 | try: 40 | model_query = Model.select().where(Model.primary_key() == record_id) 41 | except KeyError: 42 | raise RuntimeError("Could not find appropriate id to remove service record.") 43 | # remove the model instance 44 | model_query.get().delete_instance() 45 | # if we need to tell someone about what happened 46 | if notify: 47 | # publish the success event 48 | await service.event_broker.send( 49 | payload='{"status":"ok"}', 50 | action_type=change_action_status(action_type, success_status()), 51 | **message_props 52 | ) 53 | 54 | # if something goes wrong 55 | except Exception as err: 56 | # if we need to tell someone about what happened 57 | if notify: 58 | # publish the error as an event 59 | await service.event_broker.send( 60 | payload=str(err), 61 | action_type=change_action_status(action_type, error_status()), 62 | **message_props 63 | ) 64 | # otherwise we aren't supposed to notify 65 | else: 66 | # raise the exception normally 67 | raise err 68 | 69 | 70 | # return the handler 71 | return action_handler 72 | -------------------------------------------------------------------------------- /nautilus/network/events/actionHandlers/flexibleAPIHandler.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import json 3 | # local imports 4 | from nautilus.conventions.actions import intialize_service_action 5 | from nautilus.api.util import generate_api_schema 6 | 7 | async def flexible_api_handler(service, action_type, payload, props, **kwds): 8 | """ 9 | This query handler builds the dynamic picture of availible services. 10 | """ 11 | # if the action represents a new service 12 | if action_type == intialize_service_action(): 13 | # the treat the payload like json if its a string 14 | model = json.loads(payload) if isinstance(payload, str) else payload 15 | 16 | # the list of known models 17 | models = service._external_service_data['models'] 18 | # the list of known connections 19 | connections = service._external_service_data['connections'] 20 | # the list of known mutations 21 | mutations = service._external_service_data['mutations'] 22 | 23 | # if the model is a connection 24 | if 'connection' in model: 25 | # if we haven't seen the connection before 26 | if not [conn for conn in connections if conn['name'] == model['name']]: 27 | # add it to the list 28 | connections.append(model) 29 | 30 | # or if there are registered fields 31 | elif 'fields' in model and not [mod for mod in models if mod['name'] == model['name']]: 32 | # add it to the model list 33 | models.append(model) 34 | 35 | # the service could provide mutations as well as affect the topology 36 | if 'mutations' in model: 37 | # go over each mutation announce 38 | for mutation in model['mutations']: 39 | # if there isn't a mutation by the same name in the local cache 40 | if not [mut for mut in mutations if mut['name'] == mutation['name']]: 41 | # add it to the local cache 42 | mutations.append(mutation) 43 | 44 | # if there are models 45 | if models: 46 | # create a new schema corresponding to the models and connections 47 | service.schema = generate_api_schema( 48 | models=models, 49 | connections=connections, 50 | mutations=mutations, 51 | ) 52 | -------------------------------------------------------------------------------- /nautilus/network/events/actionHandlers/queryHandler.py: -------------------------------------------------------------------------------- 1 | # local imports 2 | from nautilus.conventions.actions import query_action_type, change_action_status, success_status 3 | from nautilus.api.util import parse_string 4 | 5 | async def query_handler(service, action_type, payload, props, **kwds): 6 | """ 7 | This action handler interprets the payload as a query to be executed 8 | by the api gateway service. 9 | """ 10 | # check that the action type indicates a query 11 | if action_type == query_action_type(): 12 | print('encountered query event {!r} '.format(payload)) 13 | # perform the query 14 | result = await parse_string(payload, 15 | service.object_resolver, 16 | service.connection_resolver, 17 | service.mutation_resolver, 18 | obey_auth=False 19 | ) 20 | 21 | # the props for the reply message 22 | reply_props = {'correlation_id': props['correlation_id']} if 'correlation_id' in props else {} 23 | 24 | # publish the success event 25 | await service.event_broker.send( 26 | payload=result, 27 | action_type=change_action_status(action_type, success_status()), 28 | **reply_props 29 | ) 30 | -------------------------------------------------------------------------------- /nautilus/network/events/actionHandlers/readHandler.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import json 3 | # local imports 4 | from nautilus.conventions.actions import get_crud_action, success_status, change_action_status 5 | 6 | def read_handler(Model, name=None, **kwds): 7 | """ 8 | This factory returns an action handler that responds to read requests 9 | by resolving the payload as a graphql query against the internal schema. 10 | 11 | 12 | Args: 13 | Model (nautilus.BaseModel): The model to delete when the action 14 | received. 15 | 16 | Returns: 17 | function(type, payload): The action handler for this model 18 | """ 19 | async def action_handler(service, action_type, payload, props, **kwds): 20 | # if the payload represents a new instance of `model` 21 | if action_type == get_crud_action('read', name or Model): 22 | # the props of the message 23 | message_props = {} 24 | # if there was a correlation id in the request 25 | if 'correlation_id' in props: 26 | # make sure it ends up in the reply 27 | message_props['correlation_id'] = props['correlation_id'] 28 | 29 | try: 30 | # resolve the query using the service schema 31 | resolved = service.schema.execute(payload) 32 | # create the string response 33 | response = json.dumps({ 34 | 'data': {key:value for key,value in resolved.data.items()}, 35 | 'errors': resolved.errors 36 | }) 37 | 38 | # publish the success event 39 | await service.event_broker.send( 40 | payload=response, 41 | action_type=change_action_status(action_type, success_status()), 42 | **message_props 43 | ) 44 | 45 | # if something goes wrong 46 | except Exception as err: 47 | # publish the error as an event 48 | await service.event_broker.send( 49 | payload=str(err), 50 | action_type=change_action_status(action_type, error_status()), 51 | **message_props 52 | ) 53 | 54 | 55 | # return the handler 56 | return action_handler 57 | -------------------------------------------------------------------------------- /nautilus/network/events/actionHandlers/rollCallHandler.py: -------------------------------------------------------------------------------- 1 | from nautilus.conventions.actions import roll_call_type 2 | 3 | async def roll_call_handler(service, action_type, payload, props, **kwds): 4 | """ 5 | This action handler responds to the "roll call" emitted by the api 6 | gateway when it is brought up with the normal summary produced by 7 | the service. 8 | """ 9 | # if the action type corresponds to a roll call 10 | if action_type == roll_call_type(): 11 | # then announce the service 12 | await service.announce() 13 | -------------------------------------------------------------------------------- /nautilus/network/events/actionHandlers/updateHandler.py: -------------------------------------------------------------------------------- 1 | # local imports 2 | from nautilus.conventions.actions import ( 3 | get_crud_action, 4 | change_action_status, 5 | success_status, 6 | error_status 7 | ) 8 | from nautilus.models.serializers import ModelSerializer 9 | 10 | def update_handler(Model, name=None, **kwds): 11 | """ 12 | This factory returns an action handler that updates a new instance of 13 | the specified model when a update action is recieved, assuming the 14 | action follows nautilus convetions. 15 | 16 | Args: 17 | Model (nautilus.BaseModel): The model to update when the action 18 | received. 19 | 20 | Returns: 21 | function(type, payload): The action handler for this model 22 | """ 23 | async def action_handler(service, action_type, payload, props, notify=True, **kwds): 24 | # if the payload represents a new instance of `Model` 25 | if action_type == get_crud_action('update', name or Model): 26 | try: 27 | # the props of the message 28 | message_props = {} 29 | # if there was a correlation id in the request 30 | if 'correlation_id' in props: 31 | # make sure it ends up in the reply 32 | message_props['correlation_id'] = props['correlation_id'] 33 | 34 | 35 | # grab the nam eof the primary key for the model 36 | pk_field = Model.primary_key() 37 | 38 | # make sure there is a primary key to id the model 39 | if not pk_field.name in payload: 40 | # yell loudly 41 | raise ValueError("Must specify the pk of the model when updating") 42 | 43 | # grab the matching model 44 | model = Model.select().where(pk_field == payload[pk_field.name]).get() 45 | 46 | # remove the key from the payload 47 | payload.pop(pk_field.name, None) 48 | 49 | # for every key,value pair 50 | for key, value in payload.items(): 51 | # TODO: add protection for certain fields from being 52 | # changed by the api 53 | setattr(model, key, value) 54 | 55 | # save the updates 56 | model.save() 57 | 58 | # if we need to tell someone about what happened 59 | if notify: 60 | # publish the scucess event 61 | await service.event_broker.send( 62 | payload=ModelSerializer().serialize(model), 63 | action_type=change_action_status(action_type, success_status()), 64 | **message_props 65 | ) 66 | 67 | # if something goes wrong 68 | except Exception as err: 69 | # if we need to tell someone about what happened 70 | if notify: 71 | # publish the error as an event 72 | await service.event_broker.send( 73 | payload=str(err), 74 | action_type=change_action_status(action_type, error_status()), 75 | **message_props 76 | ) 77 | # otherwise we aren't supposed to notify 78 | else: 79 | # raise the exception normally 80 | raise err 81 | 82 | # return the handler 83 | return action_handler 84 | -------------------------------------------------------------------------------- /nautilus/network/events/consumers/__init__.py: -------------------------------------------------------------------------------- 1 | from .actions import ActionHandler 2 | from .kafka import KafkaBroker 3 | -------------------------------------------------------------------------------- /nautilus/network/events/consumers/actions.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import json 3 | # local imports 4 | from .kafka import KafkaBroker 5 | 6 | 7 | class ActionHandler(KafkaBroker): 8 | 9 | consumer_channel = 'actions' 10 | producer_channel = 'actions' 11 | server = 'localhost:9092' 12 | 13 | 14 | async def handle_action(self, action_type, payload, props, **kwds): 15 | raise NotImplementedError() 16 | 17 | 18 | async def handle_message(self, **kwds): 19 | # call the user implemented function 20 | return await self.handle_action(**kwds) -------------------------------------------------------------------------------- /nautilus/network/events/consumers/api.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import json 3 | # local imports 4 | from .actions import ActionHandler 5 | from ..actionHandlers import query_handler, flexible_api_handler 6 | from ..util import combine_action_handlers 7 | 8 | class APIActionHandler(ActionHandler): 9 | """ 10 | This action handler is used by the api service to build a schema 11 | of the underlying services as they announce their existence over 12 | the action system. 13 | """ 14 | 15 | consumer_pattern = '(.*\..*\.(?!(pending)))|init|query' 16 | 17 | async def handle_action(self, *args, **kwds): 18 | 19 | # the combined handler 20 | handler = combine_action_handlers( 21 | # handle event-based queries 22 | # query_handler, 23 | # build the schema of possible services 24 | flexible_api_handler 25 | ) 26 | 27 | # pass the arguments to the combination handler 28 | await handler(self.service, *args, **kwds) 29 | -------------------------------------------------------------------------------- /nautilus/network/events/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines various utilities for dealing with the network. 3 | """ 4 | from asyncio import iscoroutinefunction, iscoroutine 5 | 6 | def combine_action_handlers(*handlers): 7 | """ 8 | This function combines the given action handlers into a single function 9 | which will call all of them. 10 | """ 11 | # make sure each of the given handlers is callable 12 | for handler in handlers: 13 | # if the handler is not a function 14 | if not (iscoroutinefunction(handler) or iscoroutine(handler)): 15 | # yell loudly 16 | raise ValueError("Provided handler is not a coroutine: %s" % handler) 17 | 18 | # the combined action handler 19 | async def combined_handler(*args, **kwds): 20 | # goes over every given handler 21 | for handler in handlers: 22 | # call the handler 23 | await handler(*args, **kwds) 24 | 25 | # return the combined action handler 26 | return combined_handler 27 | -------------------------------------------------------------------------------- /nautilus/network/http/__init__.py: -------------------------------------------------------------------------------- 1 | from .requestHandler import RequestHandler 2 | from .responses import * -------------------------------------------------------------------------------- /nautilus/network/http/requestHandler.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | 3 | class RequestHandler(web.View): 4 | """ 5 | The base class for nautilus http request handlers. 6 | 7 | Example: 8 | 9 | import nautilus 10 | from nautilus.network.http import RequestHandler, Response 11 | 12 | class MyService(nautilus.Service): pass 13 | 14 | @MyService.route('/') 15 | class MyRequestHandler(RequestHandler): 16 | async def get(self): 17 | self.finish('hello') 18 | """ 19 | 20 | async def post(self): 21 | # self.check_xsrf_cookie() 22 | pass 23 | 24 | async def options(self): 25 | return web.Response(status=204, body=b'') 26 | -------------------------------------------------------------------------------- /nautilus/network/http/responses.py: -------------------------------------------------------------------------------- 1 | from aiohttp.web import Response 2 | from aiohttp.web_exceptions import * -------------------------------------------------------------------------------- /nautilus/services/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | from .serviceManager import ServiceManager 3 | from .modelService import ModelService 4 | from .connectionService import ConnectionService 5 | from .apiGateway import APIGateway 6 | -------------------------------------------------------------------------------- /nautilus/services/serviceManager.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines a small singleton that runs various scripts in the 3 | context of the service. 4 | """ 5 | 6 | # external imports 7 | import click 8 | # local imports 9 | from ..config import Config 10 | 11 | class ServiceManager: 12 | 13 | def __init__(self, service, config=None): 14 | self.service = service 15 | self.service_config = Config(config) 16 | 17 | @click.group() 18 | def group(): 19 | pass 20 | 21 | @group.command(help="Run the service.") 22 | @click.option('--port', default=8000, help="The port for the service http server.") 23 | @click.option('--host', default='127.0.0.1', help="The host for the http server.") 24 | @click.option('--debug', default=False, is_flag=True, help="Run the service in debug mode.") 25 | def runserver(port, host, debug): 26 | # the service configuration based on cli args 27 | self.service_config.update(dict( 28 | debug=debug 29 | )) 30 | 31 | # initialize the service with the config 32 | self.service_instance = self.service(config=self.service_config) 33 | 34 | # run the service 35 | self.service_instance.run( 36 | host=host, 37 | port=int(port), 38 | ) 39 | 40 | 41 | @group.command(help="Make sure the models have been written to the db.") 42 | def syncdb(): 43 | """ Create the database entries. """ 44 | # instantiate the service before we do anything 45 | service = self.service() 46 | # get the models managed by the service 47 | models = getattr(service, 'get_models', lambda: [])() 48 | 49 | # if there are models to create 50 | if models: 51 | # for each model that we are managing 52 | for model in models: 53 | # create the table in the database 54 | model.create_table(True) 55 | 56 | # notify the user 57 | print("Successfully created necessary database tables.") 58 | 59 | # otherwise there are no tables to create 60 | else: 61 | print("There are no models to add.") 62 | 63 | 64 | @group.command(help="Drop the database tables associated with this service.") 65 | def cleardb(): 66 | """ Drop the tables associated with this service. """ 67 | # instantiate the service before we do anything 68 | service = self.service() 69 | # get the models managed by the service 70 | models = getattr(service, 'get_models', lambda: [])() 71 | 72 | # if there are models to create 73 | if models: 74 | # for each model that we are managing 75 | for model in models: 76 | # create the table in the database 77 | model.drop_table(True) 78 | 79 | # notify the user 80 | print("Successfully dropped necessary database tables.") 81 | 82 | # otherwise there are no tables to create 83 | else: 84 | print("There are no models to drop.") 85 | 86 | 87 | # save the command group to the manager 88 | self.group = group 89 | 90 | def run(self): 91 | """ run the command manager """ 92 | try: 93 | # run the command group 94 | self.group() 95 | # if there is a normal exception 96 | except Exception as err: 97 | print("Closing due to error: %s" % err) 98 | # bubble up the exception for someone else 99 | raise err 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nautilus", 3 | "version": "0.4.0", 4 | "description": "[![Join the chat at https://gitter.im/AlecAivazis/nautilus](https://badges.gitter.im/AlecAivazis/nautilus.svg)](https://gitter.im/AlecAivazis/nautilus?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs", 8 | "example": "example" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/AlecAivazis/nautilus.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/AlecAivazis/nautilus/issues" 21 | }, 22 | "homepage": "https://github.com/AlecAivazis/nautilus#readme", 23 | "devDependencies": { 24 | "babel-preset-es2015": "^6.6.0", 25 | "babel-preset-react": "^6.5.0", 26 | "babel-preset-stage-0": "^6.5.0", 27 | "babelify": "^7.2.0", 28 | "browserify": "^13.0.0", 29 | "graphiql": "^0.6.6", 30 | "graphql": "^0.4.18", 31 | "isomorphic-fetch": "^2.2.1", 32 | "react": "^0.14.7", 33 | "react-dom": "^0.14.7" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file=README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # Always prefer setuptools over distutils 4 | from setuptools import setup, find_packages 5 | 6 | setup( 7 | name='nautilus', 8 | version='0.5.2', 9 | description='A library for creating event-driven microservice applications', 10 | author='Alec Aivazis', 11 | author_email='alec@aivazis.com', 12 | url='https://github.com/AlecAivazis/nautilus', 13 | download_url='https://github.com/aaivazis/nautilus/tarball/0.5.0', 14 | keywords=['microservice', 'asyncio', 'graphql'], 15 | test_suite='nose2.collector.collector', 16 | packages=find_packages(exclude=['example', 'tests']), 17 | include_package_data=True, 18 | entry_points={'console_scripts': [ 19 | 'naut = nautilus.management:cli', 20 | ]}, 21 | install_requires=[ 22 | 'aiohttp', 23 | 'aiokafka', 24 | 'aiohttp_cors', 25 | 'aiohttp_jinja2', 26 | 'aiohttp_session', 27 | 'bcrypt', 28 | 'click', 29 | 'cryptography', 30 | 'peewee', 31 | 'graphene', 32 | 'jinja2', 33 | 'nose2', 34 | 'python-consul', 35 | 'pytest', 36 | 'PyJWT', 37 | 'uvloop', 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # external imports 4 | from os import system as run 5 | import click 6 | 7 | # the group of commands 8 | @click.group() 9 | def command_group(): pass 10 | 11 | 12 | @command_group.command() 13 | def build_api_scripts(): 14 | run('mkdir -p nautilus/api/endpoints/static/build/scripts/') 15 | # the build targets 16 | script_src = 'nautilus/api/endpoints/static/src/scripts/graphiql.js' 17 | script_build = 'nautilus/api/endpoints/static/build/scripts/graphiql.js' 18 | # babel presents 19 | presets = ' '.join(['es2015', 'react', 'stage-0']) 20 | # the build command 21 | build_cmd = 'browserify %s -t [ babelify --presets [ %s ] ]' % (script_src, presets) 22 | # the command to minify the code 23 | minify_cmd = 'uglifyjs' 24 | # minify the build and put the result in the right place 25 | run('%s | %s > %s' % (build_cmd, minify_cmd, script_build)) 26 | # let the user know we're finished 27 | print("Successfully built api script files.") 28 | 29 | 30 | @command_group.command() 31 | @click.pass_context 32 | def build_static(context): 33 | # call the underlying functions 34 | context.forward(build_api_scripts) 35 | 36 | 37 | @command_group.command() 38 | def build(docs=False): 39 | run('rm -rf dist') 40 | run('./setup.py sdist') 41 | run('./setup.py bdist_wheel') 42 | 43 | 44 | @command_group.command() 45 | def deploy(docs=False): 46 | run('twine upload dist/*') 47 | 48 | 49 | # if executing this file from the command lien 50 | if __name__ == '__main__': 51 | # start the command group 52 | command_group() 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/__init__.py -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/api/__init__.py -------------------------------------------------------------------------------- /tests/api/test_schema.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | from graphene import Schema, ObjectType, String 4 | # local imports 5 | import nautilus 6 | from ..util import async_test 7 | 8 | class TestUtil(unittest.TestSuite): 9 | 10 | def setUp(self): 11 | # create a nautilus schema to test 12 | self.schema = nautilus.api.Schema() 13 | # create an ioloop to use 14 | self.io_loop = self.get_new_ioloop() 15 | 16 | def test_does_not_auto_camel_case(self): 17 | 18 | # a query to test with a snake case field 19 | class TestQuery(ObjectType): 20 | test_field = String() 21 | 22 | def resolve_test_field(self, args, info): 23 | return 'hello' 24 | 25 | # assign the query to the schema 26 | self.schema.query = TestQuery 27 | 28 | # the query to test 29 | test_query = "query {test_field}" 30 | 31 | # execute the query 32 | resolved_query = self.schema.execute(test_query) 33 | 34 | assert 'test_field' in resolved_query.data, ( 35 | "Schema did not have snake_case field." 36 | ) 37 | 38 | assert resolved_query.data['test_field'] == 'hello', ( 39 | "Snake_case field did not have the right value" 40 | ) 41 | 42 | -------------------------------------------------------------------------------- /tests/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/auth/__init__.py -------------------------------------------------------------------------------- /tests/auth/test_fields.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | import nautilus 5 | import nautilus.models as models 6 | 7 | class TestUtil(unittest.TestCase): 8 | """ 9 | This test suite checks the behavior of the various mixins that come 10 | with nautilus. 11 | """ 12 | 13 | def setUp(self): 14 | # point the database to a in-memory sqlite database 15 | nautilus.database.init_db('sqlite:///test.db') 16 | 17 | 18 | def test_password_field(self): 19 | # create a table with a password 20 | class TestPassword(models.BaseModel): 21 | password = nautilus.auth.PasswordField() 22 | 23 | # create the table 24 | TestPassword.create_table(True) 25 | 26 | # create an instance of the table with a password 27 | record = TestPassword(password="foo") 28 | # save the record to the database 29 | record.save() 30 | 31 | # retireve the record 32 | password = TestPassword.get(TestPassword.id == record.id).password 33 | # make sure there is a hash assocaited with the password 34 | assert hasattr(password, 'hash') , ( 35 | "Retrieved record's password did not come with a hash" 36 | ) 37 | # make sure that hash hide the password 38 | assert password.hash != 'foo' , ( 39 | "Retrieved record's password is in plain sight!" 40 | ) 41 | # make sure we can check for password equality 42 | assert password == 'foo', ( 43 | 'Password could not checked for equality.' 44 | ) 45 | 46 | # remove the table from the database 47 | TestPassword.drop_table() 48 | -------------------------------------------------------------------------------- /tests/auth/test_util.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | import nautilus.models as models 5 | from nautilus.auth.util import ( 6 | generate_session_token, 7 | read_session_token 8 | ) 9 | from nautilus.auth.util.token_encryption_algorithm import token_encryption_algorithm 10 | 11 | class TestUtil(unittest.TestCase): 12 | """ 13 | This test suite checks the behavior of the various mixins that come 14 | with nautilus. 15 | """ 16 | 17 | def test_has_session_encryption_algorithm(self): 18 | # just make sure we have a value 19 | assert isinstance(token_encryption_algorithm(), str), ( 20 | "Could not retrieve session token encryption algorithm." 21 | ) 22 | 23 | 24 | def test_read_write_session_token(self): 25 | # the secret key to use 26 | secret_key = 'asdf' 27 | # generate a session token 28 | session_token = generate_session_token(secret_key, user=1) 29 | # make sure we got a string back 30 | assert isinstance(session_token, str), ( 31 | "Generated session token was not a string." 32 | ) 33 | 34 | # make sure we can read it back 35 | assert read_session_token(secret_key, session_token) == { 36 | 'user': 1 37 | }, ( 38 | "Read session token did not match expecatations." 39 | ) 40 | 41 | try: 42 | # make sure it would fail if we passed an invalid key 43 | read_session_token(secret_key, session_token) 44 | # if we got here then something went wrong 45 | raise AssertionError("Invalid key was able to read session token") 46 | except: 47 | pass -------------------------------------------------------------------------------- /tests/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/config/__init__.py -------------------------------------------------------------------------------- /tests/config/test_config.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | from nautilus.config import Config 5 | 6 | class TestUtil(unittest.TestCase): 7 | 8 | def check_configuration(self, config, message="Wrong configuration."): 9 | # make sure the configuration object looks like we expect 10 | assert config == {'foo': 'bar'} , message 11 | 12 | 13 | def test_can_read_keys_as_attribute(self): 14 | # create a config object to test 15 | config = Config(foo='bar') 16 | # validate the config object 17 | assert config.foo == 'bar', ( 18 | "Attribute could not be read" 19 | ) 20 | 21 | 22 | def test_can_set_keys_as_attrbutes(self): 23 | # create a config object to test 24 | config = Config(foo='bar') 25 | # update the attrbute 26 | config.foo = 'quz' 27 | # validate the config object 28 | assert config['foo'] == 'quz', ( 29 | "Attributes could not be updated." 30 | ) 31 | 32 | def test_can_accept_multiple_arguments(self): 33 | # create a config object with two arguments 34 | config = Config({'foo': 'bar'}, {'bar': 'baz'}) 35 | # make sure both applied 36 | assert config['foo'] == 'bar' and config['bar'] == 'baz', ( 37 | "Config could not mix in multiple values." 38 | ) 39 | 40 | 41 | def test_can_accept_kwds(self): 42 | # create a config object to test 43 | config = Config(foo='bar') 44 | # validate the config object 45 | self.check_configuration(config, 46 | "Configuration object could not accept keywords." 47 | ) 48 | 49 | 50 | def test_can_accept_dict(self): 51 | # the configuration dictionary 52 | config_dict = dict(foo='bar') 53 | # create a config object out of the dictionary 54 | config = Config(config_dict) 55 | # validate the config object 56 | self.check_configuration(config, 57 | "Configuration object could not accept dictionaries." 58 | ) 59 | 60 | 61 | def test_can_accept_type(self): 62 | # the configuration type 63 | class ConfigType: 64 | foo = 'bar' 65 | # add a function to the test too 66 | def func(self): pass 67 | 68 | # create the config object from the type 69 | config = Config(ConfigType) 70 | # validate the config object 71 | self.check_configuration(config, 72 | "Configuration object could not accept types." 73 | ) 74 | 75 | 76 | def test_can_accept_config_object(self): 77 | # create a config object 78 | config1 = Config(foo='bar') 79 | # create a config object out of that object 80 | config2 = Config(config1) 81 | # validate the config object 82 | self.check_configuration(config2, 83 | "Configuration object could not accept other config objects." 84 | ) 85 | 86 | 87 | def test_can_update_with_another_config(self): 88 | # create a config object 89 | config1 = Config(foo='bar') 90 | # create a config object out of that object 91 | config2 = Config(bar='baz') 92 | 93 | # merge the two configs 94 | config1.update({'bar':'baz'}) 95 | # make sure one can be applied on the other 96 | assert config1 == {'foo': 'bar', 'bar': 'baz'}, ( 97 | "Config could not be updated with another." 98 | ) 99 | 100 | 101 | def test_can_accept_none(self): 102 | # create a config with nothing 103 | config = Config(None) 104 | # make sure it created an empty config 105 | assert config == {}, ( 106 | "Config(None) did not create an empty config." 107 | ) 108 | -------------------------------------------------------------------------------- /tests/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/contrib/__init__.py -------------------------------------------------------------------------------- /tests/contrib/graphene_peewee/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/contrib/graphene_peewee/__init__.py -------------------------------------------------------------------------------- /tests/contrib/graphene_peewee/test_converter.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | import graphene.core.types.scalars as graphene 4 | # local imports 5 | import nautilus.models.fields as nautilus 6 | from nautilus.contrib.graphene_peewee import convert_peewee_field 7 | 8 | 9 | 10 | class TestUtil(unittest.TestCase): 11 | 12 | def assert_field_converted(self, nautilus_field, graphene_field): 13 | # convert the nautilus field to the corresponding graphene type 14 | test_graphene_type = convert_peewee_field(nautilus_field) 15 | # make sure the converted type matches the graphene field 16 | assert isinstance(test_graphene_type, graphene_field), ( 17 | "nautilus field was not properly coverted to %s" % graphene_field.__class__ 18 | ) 19 | 20 | 21 | def test_can_convert_BigIntegerField(self): 22 | self.assert_field_converted(nautilus.BigIntegerField(), graphene.Int) 23 | 24 | def test_can_convert_BooleanField(self): 25 | self.assert_field_converted(nautilus.BooleanField(), graphene.Boolean) 26 | 27 | def test_can_convert_CharField(self): 28 | self.assert_field_converted(nautilus.CharField(), graphene.String) 29 | 30 | def test_can_convert_DateField(self): 31 | self.assert_field_converted(nautilus.DateField(), graphene.String) 32 | 33 | def test_can_convert_DateTimeField(self): 34 | self.assert_field_converted(nautilus.DateTimeField(), graphene.String) 35 | 36 | def test_can_convert_DecimalField(self): 37 | self.assert_field_converted(nautilus.DecimalField(), graphene.Float) 38 | 39 | def test_can_convert_DoubleField(self): 40 | self.assert_field_converted(nautilus.DoubleField(), graphene.Float) 41 | 42 | def test_can_convert_FixedCharField(self): 43 | self.assert_field_converted(nautilus.FixedCharField(), graphene.String) 44 | 45 | def test_can_convert_FloatField(self): 46 | self.assert_field_converted(nautilus.FloatField(), graphene.Float) 47 | 48 | def test_can_convert_IntegerField(self): 49 | self.assert_field_converted(nautilus.IntegerField(), graphene.Int) 50 | 51 | def test_can_convert_PrimaryKeyField(self): 52 | self.assert_field_converted(nautilus.PrimaryKeyField(), graphene.ID) 53 | 54 | def test_can_convert_TextField(self): 55 | self.assert_field_converted(nautilus.TextField(), graphene.String) 56 | 57 | def test_can_convert_TimeField(self): 58 | self.assert_field_converted(nautilus.TimeField(), graphene.String) 59 | 60 | def test_can_convert_UUIDField(self): 61 | self.assert_field_converted(nautilus.UUIDField(), graphene.String) 62 | 63 | # def test_can_convert_ForeignKeyField(self): 64 | # self.assert_field_converted(nautilus.ForeignKeyField, graphene.ID) 65 | 66 | # def test_can_convert_BareField(self): 67 | # self.assert_field_converted(nautilus.BareField, graphene) 68 | 69 | # def test_can_convert_BlobField(self): 70 | # self.assert_field_converted() 71 | 72 | -------------------------------------------------------------------------------- /tests/contrib/graphene_peewee/test_objecttype.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | import nautilus.models as models 5 | from nautilus.contrib.graphene_peewee import PeeweeObjectType 6 | from ...util import MockModel 7 | 8 | class TestUtil(unittest.TestCase): 9 | 10 | def setUp(self): 11 | 12 | # the base model to test 13 | TestModel = MockModel() 14 | 15 | # the object type based on the models 16 | class TestObjectType(PeeweeObjectType): 17 | class Meta: 18 | model = TestModel 19 | 20 | # save the mocks to the test case 21 | self.model = TestModel 22 | self.object_type = TestObjectType 23 | 24 | 25 | def test_generated_object_has_model_fields(self): 26 | # the list of fields in the service object 27 | service_object_fields = {field.default_name \ 28 | for field in self.object_type._meta.fields} 29 | # the list of fields in the models 30 | model_fields = {field.name for field in self.model.fields()} 31 | # make sure the two lists are the same 32 | assert model_fields == service_object_fields, ( 33 | "PeeweeObjectType does not have the same fields as the model" 34 | ) 35 | -------------------------------------------------------------------------------- /tests/conventions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/conventions/__init__.py -------------------------------------------------------------------------------- /tests/conventions/test_auth.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | from nautilus.conventions.auth import cookie_name 5 | 6 | 7 | class TestUtil(unittest.TestCase): 8 | """ 9 | This test suite looks at the various utilities for manipulating 10 | models. 11 | """ 12 | 13 | def test_cookie_name(self): 14 | 15 | # save the model to the test suite 16 | assert isinstance(cookie_name(), str), ( 17 | "Could not generate string for model" 18 | ) 19 | -------------------------------------------------------------------------------- /tests/conventions/test_models.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | from nautilus.conventions.models import get_model_string, normalize_string 5 | from ..util import MockModel 6 | 7 | class TestUtil(unittest.TestCase): 8 | """ 9 | This test suite looks at the various utilities for manipulating 10 | models. 11 | """ 12 | 13 | def test_model_string(self): 14 | 15 | model = MockModel() 16 | 17 | # save the model to the test suite 18 | assert isinstance(get_model_string(model), str), ( 19 | "Could not generate string for model" 20 | ) 21 | 22 | def test_normalize_string_handles_ClassCase(self): 23 | string = 'FooBar' 24 | 25 | assert normalize_string(string) == 'fooBar', ( 26 | "ClassCase string could not be normalized" 27 | ) -------------------------------------------------------------------------------- /tests/conventions/test_services.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | import nautilus 5 | from nautilus.conventions import services as conventions 6 | from ..util import MockModel 7 | 8 | 9 | class TestUtil(unittest.TestCase): 10 | """ 11 | This test suite looks at the various utilities for manipulating 12 | models. 13 | """ 14 | 15 | def test_model_service_name(self): 16 | # a model to test with 17 | class TestModel(nautilus.models.BaseModel): 18 | name = nautilus.models.fields.CharField() 19 | TestModel = MockModel() 20 | # generate a service from the model 21 | class TestService(nautilus.ModelService): 22 | model = TestModel 23 | # make sure we could generate a name for it 24 | assert isinstance(conventions.model_service_name(TestService), str), ( 25 | "Could not generate name for model service" 26 | ) 27 | 28 | def test_model_service_name_accepts_numbers(self): 29 | # a model to test with 30 | class TestModel(nautilus.models.BaseModel): 31 | name = nautilus.models.fields.CharField() 32 | # generate a service from the model 33 | class TestService1(nautilus.ModelService): 34 | model = TestModel 35 | 36 | # figure out the conventional name for the service 37 | service_name = conventions.model_service_name(TestService1) 38 | 39 | # make sure we could generate a name for it 40 | assert ( 41 | isinstance(service_name, str) and 42 | '1' in service_name 43 | ), ( 44 | "Could not generate name for model service when it has a number." 45 | ) 46 | 47 | 48 | def test_auth_service_name(self): 49 | # make sure we can generate a name for the auth service 50 | assert isinstance(conventions.auth_service_name(), str), ( 51 | "Could not generate name for auth service." 52 | ) 53 | 54 | 55 | def test_api_gateway_name(self): 56 | # make sure we can generate a name for the auth service 57 | assert isinstance(conventions.api_gateway_name(), str), ( 58 | "Could not generate name for auth service." 59 | ) 60 | 61 | 62 | def test_connection_service_name(self): 63 | # two models to test 64 | 65 | # a connection service for both 66 | class Connection(nautilus.ConnectionService): 67 | to_service = ('TestService1',) 68 | from_service = ('TestService2',) 69 | # make sure we could make a name 70 | assert isinstance(conventions.connection_service_name(Connection()), str), ( 71 | "Could not generate name for connection service" 72 | ) -------------------------------------------------------------------------------- /tests/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/management/__init__.py -------------------------------------------------------------------------------- /tests/management/mock_template/hello: -------------------------------------------------------------------------------- 1 | {{name}} -------------------------------------------------------------------------------- /tests/management/mock_template/subdir/hello: -------------------------------------------------------------------------------- 1 | goodbye -------------------------------------------------------------------------------- /tests/management/mock_template/{{name}}: -------------------------------------------------------------------------------- 1 | hello_world -------------------------------------------------------------------------------- /tests/management/test_scripts.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | import os 4 | import subprocess 5 | import tempfile 6 | import shutil 7 | 8 | # local imports 9 | from tests.util.mock import Mock 10 | 11 | class TestUtil(unittest.TestCase): 12 | 13 | def setUp(self): 14 | # make a temporary directory 15 | self.tempdir = tempfile.mkdtemp() 16 | # save the current working directory 17 | self.cwd = os.getcwd() 18 | # change the current working directory to the temporary directory 19 | os.chdir(self.tempdir) 20 | 21 | 22 | def tearDown(self): 23 | # change the cwd back 24 | os.chdir(self.cwd) 25 | # remove the temporary directory 26 | shutil.rmtree(self.tempdir) 27 | 28 | 29 | def test_can_create_model_service(self): 30 | # import the model service creation script 31 | from nautilus.management.scripts.create import model 32 | # create a model 33 | model.callback('foo') 34 | 35 | def test_can_create_connection_service(self): 36 | # import the model service creation script 37 | from nautilus.management.scripts.create import connection 38 | # create a model 39 | connection.callback(['foo:bar']) 40 | 41 | 42 | def test_can_create_api(self): 43 | # import the model service creation script 44 | from nautilus.management.scripts.create import api 45 | # create a model 46 | api.callback() 47 | -------------------------------------------------------------------------------- /tests/management/test_util.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | import os 4 | import tempfile 5 | # local imports 6 | from tests.util.mock import Mock 7 | 8 | class TestUtil(unittest.TestCase): 9 | 10 | def test_render_template(self): 11 | # import the render utility 12 | from nautilus.management.util import render_template 13 | 14 | # the path for the mock template 15 | mock_template = os.path.join( 16 | os.path.dirname(__file__), 17 | 'mock_template' 18 | ) 19 | 20 | # open a temporary directory to render to template in 21 | with tempfile.TemporaryDirectory() as temp_dir: 22 | # render the template in the directory 23 | render_template(mock_template, temp_dir, { 24 | 'name': 'mock' 25 | }) 26 | # walk the temporary directory 27 | for dirName, subdirList, fileList in os.walk(temp_dir): 28 | # if we're looking at the temporary directory 29 | if dirName == temp_dir: 30 | # the there should be two files 31 | assert fileList == ['hello', 'mock'], ( 32 | "Root level dir did not have correct contents." 33 | ) 34 | # open the file with dynamic contents 35 | with open(os.path.join(temp_dir, 'hello')) as file: 36 | # make sure the contents are what they should be 37 | assert file.read() == 'mock', ( 38 | "Dyanmic contents did not have the correct value" 39 | ) 40 | 41 | # or if we're looking at the subdirectory 42 | elif dirName == os.path.join(temp_dir, 'subdir'): pass 43 | # otherwise we're looking at a dir we dont know about 44 | else: 45 | raise ValueError("Encountered unknown directory") 46 | -------------------------------------------------------------------------------- /tests/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/models/__init__.py -------------------------------------------------------------------------------- /tests/models/test_baseModel.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | import nautilus.models as models 5 | from ..util import MockModel 6 | 7 | 8 | class TestUtil(unittest.TestCase): 9 | """ 10 | This test suite checks for the various bits of introspective 11 | functinality supported by the base model. 12 | """ 13 | 14 | def setUp(self): 15 | 16 | self.spy = unittest.mock.MagicMock() 17 | 18 | # create a mock model 19 | self.model_record = MockModel() 20 | # create the test table 21 | self.model_record.create_table(True) 22 | 23 | self.model = self.model_record() 24 | 25 | 26 | def tearDown(self): 27 | self.model_record.drop_table() 28 | 29 | 30 | def test_can_be_saved_and_retrieved(self): 31 | # fill the model with test values 32 | self.model.name = 'foo' 33 | self.model.date = 'bar' 34 | # save it to the database 35 | self.model.save() 36 | 37 | # make sure we can get the corresponding record 38 | self.model_record.get(self.model_record.id == self.model.id) 39 | 40 | 41 | def test_can_retrieve_fields(self): 42 | # the name of the columns in the models 43 | column_names = {field.name for field in self.model_record.fields()} 44 | # check the value 45 | assert column_names == {'name', 'date', 'id'}, ( 46 | 'Model could not retrieve columns' 47 | ) 48 | 49 | 50 | def test_can_retrieve_primary_key(self): 51 | assert self.model_record.primary_key().name == 'id', ( 52 | 'Model could not return primary keys' 53 | ) 54 | 55 | 56 | def test_can_retrieve_requried_fields(self): 57 | class TestModel(self.model_record): 58 | foo = models.fields.CharField(null=False) 59 | 60 | # grab the names of the required fields 61 | required_field_names = {field.name for field in TestModel().required_fields()} 62 | # make sure it is what it should be 63 | assert required_field_names == {'id', 'foo'}, ( 64 | 'Model could not retrieve required fields.' 65 | ) 66 | 67 | 68 | def test_can_be_serialized_using_model_encoder(self): 69 | # import the model serializer 70 | from nautilus.models import ModelSerializer 71 | import json 72 | # create an instance of a model that we can serialize 73 | model = self.model_record(name="foo", date="bar") 74 | # serialize the model 75 | serialized = ModelSerializer().serialize(model) 76 | # check that the serialized model can be hydrated as expected 77 | assert json.loads(serialized) == { 78 | "name": "foo", "date": "bar", 'id': None 79 | }, ( 80 | 'Model was not correctly serialized' 81 | ) 82 | -------------------------------------------------------------------------------- /tests/models/test_mixins.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | import nautilus.models as models 5 | from ..util import Mock, MockModel 6 | 7 | 8 | class TestUtil(unittest.TestCase): 9 | """ 10 | This test suite checks the behavior of the various mixins that come 11 | with nautilus. 12 | """ 13 | 14 | def test_can_add_fields_with_mixin(self): 15 | 16 | class Mixin(models.BaseModel): 17 | address = models.fields.CharField(null=True) 18 | 19 | class TestModel(MockModel(), Mixin): pass 20 | 21 | # the name of the fields of test models 22 | field_names = {field.name for field in TestModel.fields()} 23 | 24 | # make sure the mixin was applied to the table 25 | assert field_names == {'address', 'id', 'name', 'date'}, ( 26 | 'mixin was not properly applied to model' 27 | ) 28 | 29 | def test_can_add_on_creation_handler_with_mixin(self): 30 | 31 | # a spy to check if the handler was called 32 | spy = Mock() 33 | 34 | class Mixin: 35 | @classmethod 36 | def __mixin__(cls, target): 37 | # call the spy 38 | spy(target) 39 | 40 | class TestOnCreationModel(MockModel(), Mixin): pass 41 | 42 | # verify that the mock was called with the correct arguments 43 | spy.assert_called(TestOnCreationModel) 44 | 45 | 46 | def test_multiple_mixins(self): 47 | 48 | # spies to check if the handler was called 49 | spy1 = Mock() 50 | spy2 = Mock() 51 | 52 | class MyAwesomeMixin: 53 | @classmethod 54 | def __mixin__(cls, target): 55 | # call the spy 56 | spy2(target) 57 | 58 | class MyOtherAwesomeMixin: 59 | @classmethod 60 | def __mixin__(cls, target): 61 | # call the spy 62 | spy1(target) 63 | 64 | class TestOnCreationModel(MockModel(), MyAwesomeMixin, MyOtherAwesomeMixin): 65 | pass 66 | 67 | 68 | # check that both spies were called 69 | spy1.assert_called(TestOnCreationModel) 70 | spy2.assert_called(TestOnCreationModel) 71 | -------------------------------------------------------------------------------- /tests/models/test_modelSerializer.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | from unittest.mock import MagicMock 4 | 5 | class TestUtil(unittest.TestCase): 6 | 7 | def test_can_serialize_custom_objects(self): 8 | # import the model serializer 9 | from nautilus.models.serializers import ModelSerializer 10 | # import the normal json module 11 | import json 12 | 13 | # the dummy dict to use as a serialization example 14 | inner_dict = { 15 | "foo": "bar" 16 | } 17 | 18 | class CustomClass: 19 | """ The class to test serialization with """ 20 | def _json(self): 21 | return inner_dict 22 | 23 | # serialize an instance of the custom class 24 | serialized = ModelSerializer().serialize(CustomClass()) 25 | 26 | assert isinstance(serialized, str), ( 27 | "ModelSerializer did not return a string." 28 | ) 29 | 30 | assert serialized == json.dumps(inner_dict), ( 31 | 'ModelSerializer did not return the correct string.' 32 | ) 33 | -------------------------------------------------------------------------------- /tests/models/test_utils.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | import nautilus.models as models 5 | from nautilus.conventions.services import model_service_name 6 | 7 | class TestUtil(unittest.TestCase): 8 | """ 9 | This test suite checks the behavior of the various model utilities that 10 | come with nautilus. 11 | """ 12 | 13 | def test_can_create_connection_model(self): 14 | import nautilus 15 | 16 | # models to test 17 | Model1 = model_service_name('Model1') 18 | Model2 = model_service_name('Model2') 19 | 20 | class TestConnectionService(nautilus.ConnectionService): 21 | to_service = (Model1,) 22 | from_service = (Model2,) 23 | 24 | # create the connection model 25 | connection_model = nautilus.models.create_connection_model(TestConnectionService()) 26 | 27 | assert issubclass(connection_model, models.BaseModel), ( 28 | "Generated connection model is not an instance of BaseModel" 29 | ) 30 | 31 | # grab the name of the fields 32 | connect_fields = {field.name for field in connection_model.fields()} 33 | 34 | assert connect_fields == {Model1, Model2, 'id'}, ( 35 | "Connection model did not have the correct fields." 36 | ) 37 | -------------------------------------------------------------------------------- /tests/network/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/network/__init__.py -------------------------------------------------------------------------------- /tests/network/test_action_handlers.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | from unittest.mock import Mock 4 | # local imports 5 | import nautilus 6 | import nautilus.models as models 7 | import nautilus.network.events.actionHandlers as action_handlers 8 | from ..util import async_test, Mock, MockModel 9 | 10 | class TestUtil(unittest.TestCase): 11 | 12 | def setUp(self): 13 | # point the database to a in-memory sqlite database 14 | nautilus.database.init_db('sqlite:///test.db') 15 | 16 | # save the class record 17 | self.model = MockModel() 18 | # create the table in the database 19 | nautilus.db.create_table(self.model) 20 | 21 | 22 | def tearDown(self): 23 | nautilus.db.drop_table(self.model) 24 | 25 | 26 | @async_test 27 | async def test_create_action_handler(self): 28 | # create a `create` action handler 29 | action_handler = action_handlers.create_handler(self.model) 30 | # the action type to fire the 31 | action_type = nautilus.conventions.actions.get_crud_action('create', self.model) 32 | # the attributes for the new Model 33 | payload = dict(name = 'foo') 34 | 35 | # the query for the number of matching records 36 | record_query = self.model.select().where(self.model.name=='foo') 37 | 38 | # the number of matching records before we trigger the handler 39 | assert record_query.count() == 0 40 | # call the action handler 41 | await action_handler(Mock(), action_type=action_type, payload=payload, props={}, notify=False) 42 | 43 | # make sure there is now a matching record 44 | assert record_query.count() == 1, ( 45 | "Record was not created by action handler" 46 | ) 47 | assert record_query[0].name == 'foo', ( 48 | "Record did not have the correct attribute value" 49 | ) 50 | 51 | 52 | @async_test 53 | async def test_delete_action_handler(self): 54 | # create a record in the test database 55 | record = self.model(name='foo') 56 | # save the record 57 | record.save() 58 | 59 | # create a `create` action handler 60 | action_handler = action_handlers.delete_handler(self.model) 61 | # the action type to fire the 62 | action_type = nautilus.conventions.get_crud_action('delete', self.model) 63 | # the attributes for the new Model 64 | payload = dict(id=record.id) 65 | 66 | # the query for the number of matching records 67 | record_query = self.model.select().where(self.model.id==record.id) 68 | # fire the action handler 69 | await action_handler(Mock(), action_type=action_type, payload=payload, props={}, notify=False) 70 | # make sure there aren't any queries 71 | assert record_query.count() == 0, ( 72 | "There were records matching query after it shoudl have been removed." 73 | ) 74 | 75 | 76 | @async_test 77 | async def test_update_action_handler(self): 78 | # create a record in the test database 79 | record = self.model(name='foo') 80 | # save the record 81 | record.save() 82 | # the query to grab the model we changed 83 | record_query = self.model.select().where(self.model.id == 1) 84 | # make sure the record was saved and is retrievable 85 | assert record_query.get().name == 'foo' 86 | 87 | # create a `create` action handler 88 | action_handler = action_handlers.update_handler(self.model) 89 | # the action type to fire the 90 | action_type = nautilus.conventions.get_crud_action('update', self.model) 91 | 92 | # the attributes for the new Model 93 | payload = dict(id=record.id, name='bar') 94 | 95 | # fire the action handler 96 | await action_handler(Mock(), action_type=action_type, payload=payload, props={}, notify=False) 97 | 98 | # make sure the record was changed 99 | assert record_query.get().name == 'bar', ( 100 | "Model query was not updated." 101 | ) 102 | -------------------------------------------------------------------------------- /tests/network/test_http.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | import nautilus 5 | 6 | class TestUtil(unittest.TestCase): 7 | 8 | def test_can_import_responses(self): 9 | from nautilus.network.http import HTTPOk 10 | 11 | -------------------------------------------------------------------------------- /tests/network/test_util.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from ..util import Mock, async_test 3 | 4 | class TestUtil(unittest.TestCase): 5 | 6 | @async_test 7 | async def test_can_merge_action_handlers(self): 8 | # import the function to be tested 9 | from nautilus.network.events.util import combine_action_handlers 10 | 11 | # create some handler mocks to make sure they were tested 12 | handleMock1 = Mock() 13 | handleMock2 = Mock() 14 | handleMock3 = Mock() 15 | 16 | async def asyncHandler1(*args): 17 | handleMock1(*args) 18 | async def asyncHandler2(*args): 19 | handleMock2(*args) 20 | async def asyncHandler3(*args): 21 | handleMock3(*args) 22 | 23 | # merge a series of mock handlers 24 | mergedActionHandler = combine_action_handlers( 25 | asyncHandler1, 26 | asyncHandler2, 27 | asyncHandler3, 28 | ) 29 | 30 | # the type and payload to pass to the merged handler 31 | action_type = 'foo' 32 | payload = {'foo': 'bar'} 33 | 34 | spy = Mock() 35 | 36 | # call the combined handler 37 | await mergedActionHandler(spy, action_type, payload, {}) 38 | # make sure each mock was called 39 | handleMock1.assert_called(spy, action_type, payload, {}) 40 | handleMock2.assert_called(spy, action_type, payload, {}) 41 | handleMock3.assert_called(spy, action_type, payload, {}) 42 | -------------------------------------------------------------------------------- /tests/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecAivazis/graphql-over-kafka/70e2acef27a2f87355590be1a6ca60ce3ab4d09c/tests/services/__init__.py -------------------------------------------------------------------------------- /tests/services/test_api_service.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | from collections.abc import Callable 4 | # local imports 5 | import nautilus 6 | 7 | class TestUtil(unittest.TestCase): 8 | 9 | def setUp(self): 10 | # create a service without an explict name 11 | class MyService(nautilus.APIGateway): 12 | 13 | @nautilus.auth_criteria('TestService') 14 | def test_auth(self, model, user): 15 | pass 16 | 17 | 18 | # save the service record to the test suite 19 | self.service = MyService 20 | 21 | def test_has_conventional_name(self): 22 | # make sure the name of the service matches convention 23 | assert self.service.name == nautilus.conventions.services.api_gateway_name(), ( 24 | "APIGateway did not have a name matching the convention." 25 | ) 26 | 27 | def test_has_user_password_as_model(self): 28 | # make sure the service model is the correct on 29 | assert self.service().model == nautilus.auth.models.UserPassword, ( 30 | "APIGateway did not have the correct underlying model" 31 | ) 32 | 33 | def test_has_custom_request_handler(self): 34 | # import the module to test 35 | from nautilus.api.endpoints.requestHandlers.apiQuery import APIQueryHandler 36 | # check the value of the internal attribute 37 | assert self.service().api_request_handler_class == APIQueryHandler, ( 38 | "APIGateway did not have the right query handler class" 39 | ) 40 | 41 | def test_has_custom_action_handler(self): 42 | import nautilus.network.events.consumers.api as api_handler 43 | # check the value of the internal attribute 44 | assert self.service().action_handler == api_handler.APIActionHandler, ( 45 | "APIGateway did not have the right action handler class" 46 | ) 47 | 48 | 49 | def test_requires_valid_secret_key(self): pass 50 | 51 | 52 | def test_raises_error_for_invalid_secret_key(self): pass 53 | 54 | 55 | def test_views_have_proper_cors_headers(self): pass 56 | 57 | 58 | def test_can_find_service_auth_criteria(self): 59 | # the auth criteria of the mocked service 60 | auth_criteria = self.service().auth_criteria 61 | 62 | # and it's the only one 63 | assert len(auth_criteria) == 1, ( 64 | "There is an incorrect number of entries in the auth criteria map" 65 | ) 66 | # check that the target service is in the dictionary 67 | assert 'TestService' in auth_criteria, ( 68 | "Could not find service auth criteria in service dictionary" 69 | ) 70 | # and that it's callable 71 | assert isinstance(auth_criteria['TestService'], Callable), ( 72 | "Auth criteria handler was not callable." 73 | ) 74 | -------------------------------------------------------------------------------- /tests/services/test_service.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | import nautilus 5 | from nautilus.api.endpoints import GraphQLRequestHandler 6 | from ..util import Mock 7 | 8 | class TestUtil(unittest.TestCase): 9 | 10 | def setUp(self): 11 | # create a service without an explict name 12 | class MyService(nautilus.Service): pass 13 | # save the service record to the test suite 14 | self.service = MyService 15 | 16 | 17 | def test_has_default_name(self): 18 | # make sure the name matches 19 | assert self.service.name == 'myService', ( 20 | "Service did not have the correct name." 21 | ) 22 | 23 | def test_default_name_can_have_numbers(self): 24 | # create a service without an explict name 25 | class TestService1(nautilus.Service): pass 26 | # make sure the name is what we expect 27 | assert TestService1.name == 'testService1', ( 28 | "Service did not have the correct name with number." 29 | ) 30 | 31 | 32 | def test_can_accept_name(self): 33 | class MyService(nautilus.Service): 34 | name = 'foo' 35 | 36 | assert MyService.name == 'foo', ( 37 | "Service could not recieve custom name." 38 | ) 39 | 40 | 41 | def test_can_initialize_with_schema(self): 42 | # create a mock schema 43 | schema = Mock() 44 | # make sure the internal schema is what we gave it 45 | assert self.service(schema=schema).schema == schema, ( 46 | "Service could not be initialized with a specific schema" 47 | ) 48 | 49 | 50 | def test_can_accept_config(self): 51 | # create a config object 52 | config = nautilus.Config(foo='bar') 53 | # make sure the config is what we gave it 54 | assert self.service(config=config).config == config, ( 55 | "Service could not be initialized with a specific config." 56 | ) 57 | 58 | 59 | def test_can_merge_config_from_init(self): 60 | # the config of the base class 61 | base_config = nautilus.Config(foo='bar') 62 | # the config to initialize with 63 | init_config = nautilus.Config(foo='baz', wakka='flokka') 64 | 65 | class MyConfiguredService(nautilus.Service): 66 | config = base_config 67 | 68 | # the mix of the two config 69 | mix_config = base_config.copy() 70 | mix_config.update(init_config) 71 | 72 | assert MyConfiguredService(config=init_config).config == mix_config, ( 73 | "Service could not mix the initialized config onto the base one." 74 | ) 75 | 76 | def test_has_request_handler(self): 77 | # check the value of the internal attribute 78 | assert issubclass(self.service().api_request_handler_class, GraphQLRequestHandler), ( 79 | "APIGateway did not have the right request handler class" 80 | ) 81 | 82 | 83 | def test_can_summarize(self): 84 | # the target summary 85 | target = { 86 | 'name': 'myService', 87 | } 88 | 89 | # summarize the service 90 | summarized = self.service().summarize() 91 | # make sure the names match up 92 | assert target['name'] == summarized['name'], ( 93 | "Summarzied service did not have the right name." 94 | ) -------------------------------------------------------------------------------- /tests/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .decorators import * 2 | from .mock import Mock 3 | from .mock_model import MockModel 4 | from .mock_model_service import MockModelService 5 | from .mock_connection_service import MockConnectionService -------------------------------------------------------------------------------- /tests/util/decorators.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import asyncio 3 | from unittest.mock import MagicMock 4 | # local imports 5 | from nautilus.network.events.consumers import KafkaBroker 6 | 7 | loop = asyncio.get_event_loop() 8 | 9 | def async_test(test_function): 10 | """ 11 | This decorator wraps a test function and executes it on the asyncio 12 | event loop. 13 | """ 14 | 15 | def function(*args, **kwds): 16 | # make sure the loop is open 17 | 18 | # execute the test on the event loop 19 | loop.run_until_complete(test_function(*args, **kwds)) 20 | # close the event loop 21 | loop.stop() 22 | 23 | return function -------------------------------------------------------------------------------- /tests/util/mock.py: -------------------------------------------------------------------------------- 1 | class Mock: 2 | """ 3 | This class attempts to implement most common data structure interfaces 4 | as well as provide an introspective API on actions performed in order 5 | to facilitate testing. 6 | """ 7 | 8 | def __init__(self): 9 | # the information about each call of the mock 10 | self._call_list = [] 11 | 12 | 13 | def assert_called(self, *args, **kwds): 14 | """ 15 | This function returns true if the mock has been called the 16 | designated number of times with the given arguments. 17 | """ 18 | # compute the length of the call list 19 | ncalls = len(self._call_list) 20 | # make sure the mock was called at all 21 | assert ncalls > 0, ( 22 | "Mock was never called." 23 | ) 24 | # make sure its the correct value 25 | assert ncalls == 1, ( 26 | "Mock was not called the correct number of times. " + \ 27 | "Expected {} and found {}.".format(1, ncalls) 28 | ) 29 | 30 | # use the first call to verify the criteria 31 | call = self._call_list[0] 32 | 33 | # if there are arguments to check 34 | if args: 35 | # verify they match up 36 | assert call['args'] == args, ( 37 | "Passed arguments do not match the call." 38 | ) 39 | # if there are keywords to check 40 | if kwds: 41 | assert call['kwds'] == kwds, ( 42 | "Passed keywords do not match the call." 43 | ) 44 | 45 | 46 | def __call__(self, *args, **kwds): 47 | """ 48 | This makes instances of the Mock class callable 49 | """ 50 | # add the arguments to the call number 51 | self._call_list.append({ 52 | 'args': args, 53 | 'kwds': kwds 54 | }) -------------------------------------------------------------------------------- /tests/util/mock_connection_service.py: -------------------------------------------------------------------------------- 1 | import nautilus 2 | 3 | def MockConnectionService(): 4 | 5 | """ 6 | This function returns a nautilus model to use throughout the test suite. 7 | """ 8 | 9 | class TestConnection(nautilus.ConnectionService): 10 | from_service = ('TestService',) 11 | to_service = ('AnotherTestService') 12 | 13 | # return the mocked model 14 | return TestConnection -------------------------------------------------------------------------------- /tests/util/mock_model.py: -------------------------------------------------------------------------------- 1 | def MockModel(name='TestModel'): 2 | 3 | """ 4 | This function returns a nautilus model to use throughout the test suite. 5 | """ 6 | # local imports 7 | from nautilus import models 8 | 9 | # return the mocked model 10 | return type(name, (models.BaseModel,), { 11 | 'name': models.fields.CharField(null=True), 12 | 'date': models.fields.CharField(null=True) 13 | }) -------------------------------------------------------------------------------- /tests/util/mock_model_service.py: -------------------------------------------------------------------------------- 1 | import nautilus 2 | from .mock_model import MockModel 3 | 4 | def MockModelService(): 5 | class MockService(nautilus.ModelService): 6 | model = MockModel() 7 | 8 | return MockService -------------------------------------------------------------------------------- /tests/util/tests/test_mock.py: -------------------------------------------------------------------------------- 1 | # external imports 2 | import unittest 3 | # local imports 4 | from tests.util.mock import Mock 5 | 6 | class TestUtil(unittest.TestCase): 7 | 8 | def setUp(self): 9 | # create a mock 10 | self.mock = Mock() 11 | 12 | def test_must_be_called_more_than_once(self): 13 | try: 14 | # check that the mock has been called 15 | self.mock.assert_called() 16 | # it throws an assertion error 17 | except AssertionError: 18 | pass 19 | 20 | 21 | def test_default_fails_multiple_calls(self): 22 | # call the mock twice 23 | self.mock() 24 | self.mock() 25 | 26 | # expect this check to fail 27 | try: 28 | # check that the mock has been called 29 | self.mock.assert_called() 30 | # it throws an assertion error 31 | except AssertionError: 32 | pass 33 | 34 | 35 | def test_can_check_for_args(self): 36 | # pass some args to the mock 37 | self.mock('bar', 'baz') 38 | # verify that the mock was called with the args 39 | self.mock.assert_called('bar', 'baz') 40 | 41 | 42 | def test_can_check_for_kwds(self): 43 | # pass some kwds to the mock 44 | self.mock(foo='bar') 45 | # verify that th emock was called with the args 46 | self.mock.assert_called(foo='bar') 47 | --------------------------------------------------------------------------------