From c820bd6bec92f25ce08584ef927589fc471c4b50 Mon Sep 17 00:00:00 2001 From: GSmithApps <14.gsmith.14@gmail.com> Date: Sat, 24 Aug 2024 23:03:39 -0500 Subject: [PATCH] Added geocoding python tutorial. --- .../python/hello_world_in_python/index.md | 2 +- docs/tutorials/python/geocoding-app/index.md | 405 ++++++++++++++++++ 2 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 docs/tutorials/python/geocoding-app/index.md diff --git a/docs/getting_started/python/hello_world_in_python/index.md b/docs/getting_started/python/hello_world_in_python/index.md index 282f7751..9f4f264a 100644 --- a/docs/getting_started/python/hello_world_in_python/index.md +++ b/docs/getting_started/python/hello_world_in_python/index.md @@ -165,7 +165,7 @@ You can pass multiple inputs to a Workflow, but it's a good practice to send a s ::: -The method calls the `workflow.execute_activty` method which executes an Activity called `say_hello`, which you'll define next. `workflow.execute_activity` needs the [Activity Type](https://docs.temporal.io/activities#activity-type), the input parameters for the Activity, and a [Start-To-Close Timeout](https://docs.temporal.io/activities#start-to-close-timeout) or [Schedule-To-Close Timeout](https://docs.temporal.io/concepts/what-is-a-schedule-to-close-timeout). +The method calls the `workflow.execute_activity` method which executes an Activity called `say_hello`, which you'll define next. `workflow.execute_activity` needs the [Activity Type](https://docs.temporal.io/activities#activity-type), the input parameters for the Activity, and a [Start-To-Close Timeout](https://docs.temporal.io/activities#start-to-close-timeout) or [Schedule-To-Close Timeout](https://docs.temporal.io/concepts/what-is-a-schedule-to-close-timeout). Finally, the `run` method returns the result of the Activity Execution. diff --git a/docs/tutorials/python/geocoding-app/index.md b/docs/tutorials/python/geocoding-app/index.md new file mode 100644 index 00000000..22f431c3 --- /dev/null +++ b/docs/tutorials/python/geocoding-app/index.md @@ -0,0 +1,405 @@ +--- +id: build-a-geocoding-application-python +title: Build a geocoding applicaton with Python +sidebar_position: 5 +description: You will implement a geocoding application in Python that gets input from a user and calls a Rest API. +keywords: [Python, temporal, sdk, tutorial] +tags: +- Python +image: /img/temporal-logo-twitter-card.png +last_update: + date: 2024-08-15 +--- + +![Temporal Python SDK](/img/sdk_banners/banner_python.png) + +### Introduction + +When it comes to building business process applications, coordinating +all parts of the application from user interaction to API calls can be +complex. +Temporal shields you from these issues by providing +reliability and operability. + +In this tutorial, you'll build an application +that does standard business tasks, such as getting input from a user and querying an API. +Specifically, the application will ask the user for an API key and an address, then +it will geocode the address using Geoapify. + +## Prerequisites + +Before starting this tutorial, complete the following 5 steps: + +1. Complete the tutorial to [Set up a local development environment](/getting_started/python/dev_environment/index.md). +2. Complete the [Hello World](/getting_started/python/hello_world_in_python/index.md) tutorial. +3. Install [requests](https://requests.readthedocs.io/en/latest/) (tested with version 2.32.3). + +```command +pip install requests +``` + +4. Get a [Geoapify](https://www.geoapify.com) API key. +5. Be sure the Temporal Service is running. If it isn't, run `temporal server start-dev` to start the Temporal Service. + +Now that you have your environment ready, it's time to build an +invincible geocoder. + +## Develop a Workflow to orchestrate your interactions with the user and the API + +In this application, the Workflow coordinates the Activities of +getting information from the user and querying the API. + +Create a new file called `workflow.py` and add +the following code: + + +[workflow.py](https://github.com/GSmithApps/temporal-project-tutorial/blob/master/workflow.py) +```py +from datetime import timedelta +from temporalio import workflow + +# Import activity, passing it through the sandbox without reloading the module +with workflow.unsafe.imports_passed_through(): + from activities import ( + get_address_from_user, + get_api_key_from_user, + get_lat_long, + QueryParams, + ) + +_TIMEOUT_5_MINS = 5 * 60 + +# Decorator for the workflow class. +# This must be set on any registered workflow class. +@workflow.defn +class GeoCode: + """The Workflow. Orchestrates the Activities.""" + + # Decorator for the workflow run method. + # This must be set on one and only one async method defined on the same class as @workflow.defn + @workflow.run + async def run(self) -> list: + """The run method of the Workflow.""" + + api_key_from_user = await workflow.execute_activity( + get_api_key_from_user, + start_to_close_timeout=timedelta(seconds=_TIMEOUT_5_MINS), + ) + + address_from_user = await workflow.execute_activity( + get_address_from_user, + start_to_close_timeout=timedelta(seconds=_TIMEOUT_5_MINS), + ) + + query_params = QueryParams(api_key=api_key_from_user, address=address_from_user) + + lat_long = await workflow.execute_activity( + get_lat_long, + query_params, + start_to_close_timeout=timedelta(seconds=_TIMEOUT_5_MINS), + ) + + return lat_long + + +``` + + +In this code snippet, you decorated the `GeoCode` class with `@workflow.defn`, which +tells Temporal that the class is a Workflow. + +You decorated the `run()` method with `@workflow.run`, which tells +Temporal that this method is the Workflow's run method. As mentioned in the code comment, +you apply this decorator to exactly one method in the Workflow. + +You passed the Activities into the calls to `workflow.execute_activity(activity)`. +If the Activities need arguments, pass them in after the Activity using +`workflow.execute_activity(activity, args*, ...)`, as shown in `get_lat_long()`. It's +recommended to collapse multiple arguments into a single argument using a dataclass, +which you did here with `QueryParams`. + +With the skeleton in place, you can now develop the Activities. + +## Develop Activities to interact with the user and the API + +In this section, you'll implement the Activities that interact +with the outside world. The Workflow is doing the orchestration, +and the Activities are doing the atomic actions. + +Add the following to a new file called `activities.py`: + + +[activities.py](https://github.com/GSmithApps/temporal-project-tutorial/blob/master/activities.py) + +```py +from temporalio import activity + + +# Tells Temporal that this is an Activity +@activity.defn +async def get_api_key_from_user() -> str: + return input("Please give your API key: ") + + +# Tells Temporal that this is an Activity +@activity.defn +async def get_address_from_user() -> str: + return input("Please give an address: ") + + +``` + + +The `@activity.defn` decorator tells Temporal +that this function is an Activity. + +You call these Activities in the Workflow you made earlier. After the Workflow +calls them, it has the user's API key and address. Next, +it calls an Activity called `get_lat_long`, with an argument of +type `QueryParams`. You'll implement that next. + +Add the following +to the end of the `activities.py` file that you just made: + + +[activities.py](https://github.com/GSmithApps/temporal-project-tutorial/blob/master/activities.py) +```py +import requests +from dataclasses import dataclass + +@dataclass +class QueryParams: + api_key: str + address: str + +@activity.defn +async def get_lat_long(query_params: QueryParams) -> list: + base_url = "https://api.geoapify.com/v1/geocode/search" + + params = { + "text": query_params.address, + "apiKey": query_params.api_key + } + + response = requests.get(base_url, params=params, timeout=1000) + + response_json = response.json() + + lat_long = response_json['features'][0]['geometry']['coordinates'] + + return lat_long +``` + + +As mentioned before, condense the arguments to Activities into a +dataclass. In this case, the Activity is +an API call that needs the user's location and API key, so you'll +bundle those as a data class. That's `QueryParams`. + +Now that you have the Workflow and Actions, you need to run them. In +the next section, you'll begin that process by making and running a Worker. + +## Create and run a Worker to host your Workflow and Activities + +The Worker +is the process that connects to the Temporal Service and listens +on a Task Queue. Here is how you can make +a Worker factory. + +Make a new file called `make_worker.py` and +enter the following: + + +[make_worker.py](https://github.com/GSmithApps/temporal-project-tutorial/blob/master/make_worker.py) +```py +from temporalio.client import Client +from temporalio.worker import Worker + +from activities import get_address_from_user, get_api_key_from_user, get_lat_long +from workflow import GeoCode + + +def make_worker(client: Client): + + worker = Worker( + client, + task_queue="geocode-task-queue", + workflows=[GeoCode], + activities=[get_address_from_user, get_api_key_from_user, get_lat_long] + ) + + return worker +``` + + +The arguments are the following: + +- `client`, is the connection to the Temporal Service. +- `task_queue` is the Task Queue that the Worker listens on (later, +when you run the Workflow, you'll put items on that Task Queue). +- `workflows` is the list of Workflows it can process. +- `activities` is a list of Activities that it can work on. + +Now that you have a factory to make a Worker, it's time to connect to the +Temporal Service, use that factory to make a Worker, and run the Worker. + +Make a new file called `run_worker.py` and enter the following: + + +[run_worker.py](https://github.com/GSmithApps/temporal-project-tutorial/blob/master/run_worker.py) +```py +import asyncio + +from temporalio.client import Client + +from make_worker import make_worker + + +async def main(): + + client = await Client.connect("localhost:7233", namespace="default") + + worker = make_worker(client) + + await worker.run() + +if __name__ == "__main__": + asyncio.run(main()) +``` + + +This snippet connects to the Temporal Service using `Client.connect`. +For this to work, the Temporal Service +needs to be running, as mentioned in the prerequisites. + +Next, the code passes that `client` into the `make_worker` function you made earlier. +This returns a Worker, which you use to +call the `.run()` method. This is what makes the Worker run +and start listening for work on the Task Queue. You will run it now. + +1. Open a new terminal (keep the service running + in a different terminal). +2. Navigate to the project directory, and run + the following command (it won't output anything yet). + +```command +python run_worker.py +``` + +It will start listening, but it has nothing +to do yet because there is nothing on the queue. You will +fix that in the next section by running the Workflow. + +## Run the Workflow to execute the application + +The last piece is executing the Workflow. + +Enter the following code in a new file called `run_workflow.py`. + + +[run_workflow.py](https://github.com/GSmithApps/temporal-project-tutorial/blob/master/run_workflow.py) +```py +import asyncio + +from workflow import GeoCode +from temporalio.client import Client + + +async def main(): + # Create a client connected to the server at the given address + client = await Client.connect("localhost:7233") + + # Execute a workflow + lat_long = await client.execute_workflow( + GeoCode.run, id="geocode-workflow", task_queue="geocode-task-queue" + ) + + print(f"Lat long: {lat_long}") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + + +In this piece, you connect to the service, then call `execute_workflow()`. +Its arguments are the following: + +- The Workflow method that you decorated with `@workflow.defn`. +- The ID for the Workflow. +- The Task Queue. + +You're ready to run the code. Do the following 4 steps: + +1. Open a third terminal (the other + two processes should still be running). +2. Navigate to the project + directory, and run the following command: + +```command +python run_workflow.py +``` + +At this point, the +application is running. If you look at the terminal +that's running the Worker (not the terminal that's running the Workflow), +it should be asking you for your +API key. + +3. Enter the Geoapify API key mentioned in the prerequisites. + +Next, it will ask you for an address. + +4. Enter an address. + +It will query Geoapify to +geocode the address, and it will print the latitude and longitude +to the terminal running the Workflow. + +## Conclusion + +You have built a business process application that +runs invincibly with Temporal. + +For more detail on how Temporal can help business process +applications, please see Temporal's Chief Product Officer's discussion +in [The Top 3 Use Cases for Temporal (ft. Temporal PMs)](https://www.youtube.com/watch?v=eMf1fk9RmhY&t=299s). + +### Next Steps + +Try defining a Retry Policy and applying it to the Activities. + +
+ +What does Temporal recommend to do in Activities instead of in +Workflows? Which ones did you do in this tutorial? + +Activities perform anything that may be non-deterministic, may fail, +or may have side effects. This could be writing to +disk, reading from a database, or getting information +from a user. In this Activity, you got input from the user via +the command line, and you queried a REST API. + +
+ +
+ +What pieces of information does a Worker need when instantiated? This +example had four. + + + +
+ +
+ +How do you denote that a piece of Python code is a Workflow? + +You decorate a class with the @workflow.defn decorator, and you decorate exactly one of its methods +with @workflow.run. +