diff --git a/03_cr_connect_workflow/01_scripts/00_index.rst b/03_cr_connect_workflow/01_scripts/00_index.rst new file mode 100644 index 0000000..bb1c0d0 --- /dev/null +++ b/03_cr_connect_workflow/01_scripts/00_index.rst @@ -0,0 +1,24 @@ +.. _index-scripts: + +================= +Creating a Script +================= + +`Scripts` can be called from a script task in workflows. They are a great way to extend workflow capabilities. + +Scripts extend the `Script` base class found in crc.scripts.script, and they must define the +`get_description`, `do_task`, and `do_task_validate_only` methods. + +The get_description method should return a string that is displayed when configurators get a list of available scripts. + +The do_task_validate_only method is run during the configurator `shield test`. +We only need to make sure that the script *can* run here. We don't want to make any externals calls. + +The do_task method is where we put the code we need to achieve our task. + +.. toctree:: + :maxdepth: 2 + + 01_script + 02_workflow + 03_running diff --git a/03_cr_connect_workflow/01_scripts/01_script.rst b/03_cr_connect_workflow/01_scripts/01_script.rst new file mode 100644 index 0000000..78b5b32 --- /dev/null +++ b/03_cr_connect_workflow/01_scripts/01_script.rst @@ -0,0 +1,57 @@ +-------------- +Example Script +-------------- + +Create a script that accepts keyword arguments, calls an external api using the arguments, and returns a result. + +My data source is a simple API found at https://deckofcardsapi.com that returns cards drawn from one or more decks of playing cards. + +My script accepts two keyword arguments; cards and decks. + + +Example code: crc/scripts/tutorial.py + +.. code-block:: Python + + from crc.scripts.script import Script + import requests + + + class TutorialScript(Script): + + def get_description(self): + return """Simple script for teaching purposes""" + + def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): + self.do_task(task, study_id, workflow_id, *args, **kwargs) + + def do_task(self, task, study_id, workflow_id, *args, **kwargs): + + drawn_cards = [] + + cards = int(kwargs['cards']) if hasattr(kwargs, 'cards') else 1 + decks = int(kwargs['decks']) if hasattr(kwargs, 'decks') else 1 + + deck_url = f'https://deckofcardsapi.com/api/deck/new/shuffle/?deck_count={decks}' + deck_response = requests.get(deck_url) + deck_id = deck_response.json()['deck_id'] + + card_url = f'https://deckofcardsapi.com/api/deck/{deck_id}/draw/?count={cards}' + card_response = requests.get(card_url) + + for card in range(cards): + card_value = card_response.json()['cards'][card]['value'] + card_suit = card_response.json()['cards'][card]['suit'] + drawn_cards.append({'suit': card_suit, 'value': card_value}) + + return drawn_cards + + +First, I create an empty list of drawn_cards, and pull cards and decks from the keyword arguments. +Cards and decks both default to 1 if they don't exist as keyword arguments. + +I make an API call to get a `deck_id`, and then make another call with the deck_id to get a `card_response`. + +From the card_response, I build and return a list of drawn_cards. + +We have a script we can call from a workflow. Now we have to build the workflow. diff --git a/03_cr_connect_workflow/01_scripts/02_workflow.rst b/03_cr_connect_workflow/01_scripts/02_workflow.rst new file mode 100644 index 0000000..7242fae --- /dev/null +++ b/03_cr_connect_workflow/01_scripts/02_workflow.rst @@ -0,0 +1,15 @@ +---------------- +Example Workflow +---------------- + +Here is a simple workflow you can use to call this script. + +.. image:: /_static/03/01/tutorial_workflow.png + +.. image:: /_static/03/01/how_many.png + +.. image:: /_static/03/01/get_cards.png + +.. image:: /_static/03/01/display_cards.png + +You can find the :download:`bpmn for the workflow ` in the repository. diff --git a/03_cr_connect_workflow/01_scripts/03_running.rst b/03_cr_connect_workflow/01_scripts/03_running.rst new file mode 100644 index 0000000..20508fb --- /dev/null +++ b/03_cr_connect_workflow/01_scripts/03_running.rst @@ -0,0 +1,13 @@ +======= +Running +======= + +And here it is running. + +.. image:: /_static/03/01/start_workflow.png + +.. image:: /_static/03/01/enter_how_many.png + +.. image:: /_static/03/01/display_returned_cards.png + + diff --git a/03_cr_connect_workflow/02_services/00_index.rst b/03_cr_connect_workflow/02_services/00_index.rst new file mode 100644 index 0000000..72f7a30 --- /dev/null +++ b/03_cr_connect_workflow/02_services/00_index.rst @@ -0,0 +1,16 @@ +.. _index-services: + +================== +Building a Service +================== + +Services are internal code related to a specific function. We have services that manage users and studies, +interact with Protocol Builder and the LDAP server, send emails, and make calls to SpiffWorkflow. + +Services should not be called directly from outside the system. They should be called by scripts, the API, and other services. + +.. toctree:: + :maxdepth: 2 + + 01_service + 02_script diff --git a/03_cr_connect_workflow/02_services/01_service.rst b/03_cr_connect_workflow/02_services/01_service.rst new file mode 100644 index 0000000..5314f2e --- /dev/null +++ b/03_cr_connect_workflow/02_services/01_service.rst @@ -0,0 +1,30 @@ +---------------- +Tutorial Service +---------------- + +Let's build a service that replaces the do_task method in our script. We can then call the service from our script. + +.. code-block:: Python + + import requests + + + class TutorialService(object): + + @staticmethod + def pick_a_card(cards, decks): + drawn_cards = [] + + deck_url = f'https://deckofcardsapi.com/api/deck/new/shuffle/?deck_count={decks}' + deck_response = requests.get(deck_url) + deck_id = deck_response.json()['deck_id'] + + card_url = f'https://deckofcardsapi.com/api/deck/{deck_id}/draw/?count={cards}' + card_response = requests.get(card_url) + + for card in range(cards): + card_value = card_response.json()['cards'][card]['value'] + card_suit = card_response.json()['cards'][card]['suit'] + drawn_cards.append({'suit': card_suit, 'value': card_value}) + + return drawn_cards diff --git a/03_cr_connect_workflow/02_services/02_script.rst b/03_cr_connect_workflow/02_services/02_script.rst new file mode 100644 index 0000000..1fcb36f --- /dev/null +++ b/03_cr_connect_workflow/02_services/02_script.rst @@ -0,0 +1,26 @@ +============= +Modify Script +============= + +We can now modify our script to call this new service. + +.. code-block:: Python + + from crc.scripts.script import Script + from crc.services.tutorial_service import TutorialService + + class TutorialScript(Script): + + def get_description(self): + return """Simple script for teaching purposes""" + + def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): + self.do_task(task, study_id, workflow_id, *args, **kwargs) + + def do_task(self, task, study_id, workflow_id, *args, **kwargs): + + cards = kwargs['cards'] + decks = kwargs['decks'] + + drawn_cards = TutorialService.pick_a_card(cards=cards, decks=decks) + return drawn_cards diff --git a/03_cr_connect_workflow/03_api/00_index.rst b/03_cr_connect_workflow/03_api/00_index.rst new file mode 100644 index 0000000..b528801 --- /dev/null +++ b/03_cr_connect_workflow/03_api/00_index.rst @@ -0,0 +1,19 @@ +.. _index-api: + +====================== +Adding an API Endpoint +====================== + +Currently, we have a service that returns playing cards. +We can call this service from a script, and return the result in a workflow. + +Now, let's add an API endpoint that calls the service. + +There are two steps in the process; defining the endpoint, and adding the Python code to support it. + +.. toctree:: + :maxdepth: 2 + + 01_yml + 02_get_cards + 03_endpoints diff --git a/03_cr_connect_workflow/03_api/01_yml.rst b/03_cr_connect_workflow/03_api/01_yml.rst new file mode 100644 index 0000000..a7c4f2c --- /dev/null +++ b/03_cr_connect_workflow/03_api/01_yml.rst @@ -0,0 +1,86 @@ +------- +api.yml +------- + +The API endpoints are defined in `api.yml` in the `paths` section. + +Endpoints all have a `path`. Our path will be `get_cards`. +Endpoints can have `parameters`. Our parameters are `cards` and `decks`. +Our parameters are included as part of the query. +They are integers, and are not required. + +The request type for our endpoint is `GET`. +We tell the endpoint what Python code to execute in `operationId`. +We will turn off `security` for our endpoint, and add a `Configurator Tools` tag. + +We must define a `200` response. +In the response, declare that we are returning an `array` of `PlayingCards`. + + +Here is what that code looks like: + +.. code-block:: + + paths: + + ... + + /get_cards: + parameters: + - name: cards + in: query + required: false + description: The number of cards to draw. Defaults to one. + schema: + type: integer + - name: decks + in: query + required: false + description: The number of decks to draw from. Defaults to one. + schema: + type: integer + get: + operationId: crc.api.tools.get_cards + security: [] # Disable security for this endpoint only. + summary: Draw cards from a deck of playing cards. For learning purposes only. + tags: + - Configurator Tools + responses: + '200': + description: Returns the chosen card(s) + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PlayingCards" + +We need to define the schema for PlayingCards. + +Our playing cards have two properties; suit and value. +They are both strings. + +You can add an example which shows up on the API webpage. + +Schemas are defined in the `schemas` section. + +The schema code looks like this: + +.. code-block:: + + schemas: + + ... + + PlayingCards: + properties: + suit: + type: string + example: SPADES + value: + type: string + example: 10 + +We use the `Connexion `_ framework for our API. + +You can check your YML code at https://editor.swagger.io/ diff --git a/03_cr_connect_workflow/03_api/02_get_cards.rst b/03_cr_connect_workflow/03_api/02_get_cards.rst new file mode 100644 index 0000000..68c1982 --- /dev/null +++ b/03_cr_connect_workflow/03_api/02_get_cards.rst @@ -0,0 +1,38 @@ +We now need to write the Python code that should exist in `crc.api.tools.get_cards`. + +----------------------- +crc.api.tools.get_cards +----------------------- + +We have defined an API endpoint. Now, we need to write the Python code to support it. + +When we defined the get_cards endpoint, we set `operationId` to `crc.api.tools.get_cards`. + +.. code-block:: + + get: + operationId: crc.api.tools.get_cards + +This means that the API expects a method called `get_cards` in the `crc.api.tools` module. + +We need to add this method. + +get_cards +--------- + +We have already done the heavy lifting, so we don't have to write much Python code. + +get_cards has to: + +- take in two parameters; cards and decks, +- make a call to our service, and +- return a list of playing cards. + +.. code-block:: + + def get_cards(cards=1, decks=1): + drawn_cards = TutorialService.pick_a_card(cards=cards, decks=decks) + return drawn_cards + +Remember that our API definition says that our parameters are not required. +So, we had to give them default values in our method definition. diff --git a/03_cr_connect_workflow/03_api/03_endpoints.rst b/03_cr_connect_workflow/03_api/03_endpoints.rst new file mode 100644 index 0000000..381441c --- /dev/null +++ b/03_cr_connect_workflow/03_api/03_endpoints.rst @@ -0,0 +1,7 @@ +--------- +Endpoints +--------- + +At this point, we should have a functioning API endpoint that calls crc.api.tools.get_cards. + +If you restart your instance, you can view the endpoint at http://localhost:5000/v1.0/ui/#/Configurator%20Tools/