Package: temporalio
Temporalis a distributed, scalable, durable, and highly available orchestration engine used to
execute asynchronous, long-running business logic in a scalable and resilient way."Temporal Python SDK" is the framework for authoring workflows and activities using the Python programming language.Also see:Application Development Guide- Once you've tried ourQuick Start, check out our guide on how to use Temporal in your Python applications, including
information around Temporal core concepts.Python Code SamplesAPI Documentation- Complete Temporal Python SDK Package reference.In addition to features common across all Temporal SDKs, the Python SDK also has the following interesting features:Type SafeThis library uses the latest typing and MyPy support with generics to ensure all calls can be typed. For example,
starting a workflow with anintparameter when it accepts astrparameter would cause MyPy to fail.Different Activity TypesThe activity worker has been developed to work withasync def, threaded, and multiprocess activities. Whileasync defactivities are the easiest and recommended, care has been taken to make heartbeating and cancellation also
work across threads/processes.CustomasyncioEvent LoopThe workflow implementation basically turnsasync deffunctions into workflows backed by a distributed, fault-tolerant
event loop. This means task management, sleep, cancellation, etc have all been developed to seamlessly integrate withasyncioconcepts.See theblog postintroducing the Python SDK for an
informal introduction to the features and their implementation.ContentsQuick StartInstallationImplementing a WorkflowRunning a WorkflowNext StepsUsageClientData ConversionCustom Type Data ConversionWorkersWorkflowsDefinitionRunningInvoking ActivitiesInvoking Child WorkflowsTimersConditionsAsyncio and CancellationWorkflow UtilitiesExceptionsExternal WorkflowsTestingAutomatic Time SkippingManual Time SkippingMocking ActivitiesWorkflow SandboxHow the Sandbox WorksAvoiding the SandboxCustomizing the SandboxPassthrough ModulesInvalid Module MembersKnown Sandbox IssuesGlobal Import/BuiltinsSandbox is not SecureSandbox PerformanceExtending Restricted ClassesCertain Standard Library Calls on Restricted Objectsis_subclass of ABC-based Restricted ClassesCompiled Pydantic Sometimes Using Wrong TypesActivitiesDefinitionTypes of ActivitiesSynchronous ActivitiesSynchronous Multithreaded ActivitiesSynchronous Multiprocess/Other ActivitiesAsynchronous ActivitiesActivity ContextHeartbeating and CancellationWorker ShutdownTestingWorkflow ReplayOpenTelemetry SupportProtobuf 3.x vs 4.xKnown Compatibility Issuesgevent PatchingDevelopmentBuildingPrepareBuildUseLocal SDK development environmentTestingProto Generation and TestingStyleQuick StartWe will guide you through the Temporal basics to create a "hello, world!" script on your machine. It is not intended as
one of the ways to use Temporal, but in reality it is very simplified and decidedly not "the only way" to use Temporal.
For more information, check out the docs references in "Next Steps" below the quick start.InstallationInstall thetemporaliopackage fromPyPI.These steps can be followed to use with a virtual environment andpip:Create a virtual environmentUpdatepip-python -m pip install -U pipNeeded because older versions ofpipmay not pick the right wheelInstall Temporal SDK -python -m pip install temporalioThe SDK is now ready for use. To build from source, see "Building" near the end of this documentation.NOTE: This README is for the current branch and not necessarily what's released onPyPI.Implementing a WorkflowCreate the following inactivities.py:fromtemporalioimportactivity@activity.defndefsay_hello(name:str)->str:returnf"Hello,{name}!"Create the following inworkflows.py:fromdatetimeimporttimedeltafromtemporalioimportworkflow# Import our activity, passing it through the sandboxwithworkflow.unsafe.imports_passed_through():from.activitiesimportsay_hello@workflow.defnclassSayHello:@workflow.runasyncdefrun(self,name:str)->str:returnawaitworkflow.execute_activity(say_hello,name,schedule_to_close_timeout=timedelta(seconds=5))Create the following inrun_worker.py:importasyncioimportconcurrent.futuresfromtemporalio.clientimportClientfromtemporalio.workerimportWorker# Import the activity and workflow from our other filesfrom.activitiesimportsay_hellofrom.workflowsimportSayHelloasyncdefmain():# Create client connected to server at the given addressclient=awaitClient.connect("localhost:7233")# Run the workerwithconcurrent.futures.ThreadPoolExecutor(max_workers=100)asactivity_executor:worker=Worker(client,task_queue="my-task-queue",workflows=[SayHello],activities=[say_hello],activity_executor=activity_executor,)awaitworker.run()if__name__=="__main__":asyncio.run(main())Assuming you have aTemporal server running on localhost, this
will run the worker:python run_worker.pyRunning a WorkflowCreate the following script atrun_workflow.py:importasynciofromtemporalio.clientimportClient# Import the workflow from the previous codefrom.workflowsimportSayHelloasyncdefmain():# Create client connected to server at the given addressclient=awaitClient.connect("localhost:7233")# Execute a workflowresult=awaitclient.execute_workflow(SayHello.run,"my name",id="my-workflow-id",task_queue="my-task-queue")print(f"Result:{result}")if__name__=="__main__":asyncio.run(main())Assuming you haverun_worker.pyrunning from before, this will run the workflow:python run_workflow.pyThe output will be:Result: Hello, my-name!Next StepsTemporal can be implemented in your code in many different ways, to suit your application's needs. The links below will
give you much more information about how Temporal works with Python:Code Samples- If you want to start with some code, we have provided
some pre-built samples.Application Development GuideOur Python specific
Developer's Guide will give you much more information on how to build with Temporal in your Python applications than
our SDK README ever could (or should).API Documentation- Full Temporal Python SDK package documentation.UsageFrom here, you will find reference documentation about specific pieces of the Temporal Python SDK that were built around
Temporal concepts.This section is not intended as a how-to guide-- For more how-to oriented information, check out
the links in theNext Stepssection above.ClientA client can be created and used to start a workflow like so:fromtemporalio.clientimportClientasyncdefmain():# Create client connected to server at the given address and namespaceclient=awaitClient.connect("localhost:7233",namespace="my-namespace")# Start a workflowhandle=awaitclient.start_workflow(MyWorkflow.run,"some arg",id="my-workflow-id",task_queue="my-task-queue")# Wait for resultresult=awaithandle.result()print(f"Result:{result}")Some things to note about the above code:AClientdoes not have an explicit "close"To enable TLS, thetlsargument toconnectcan be set toTrueor aTLSConfigobjectA single positional argument can be passed tostart_workflow. If there are multiple arguments, only the
non-type-safe form ofstart_workflowcan be used (i.e. the one accepting a string workflow name) and it must be in
theargskeyword argument.Thehandlerepresents the workflow that was started and can be used for more than just getting the resultSince we are just getting the handle and waiting on the result, we could have calledclient.execute_workflowwhich
does the same thingClients can have many more options not shown here (e.g. data converters and interceptors)A string can be used instead of the method reference to call a workflow by name (e.g. if defined in another language)Clients do not work across forksClients also provide a shallow copy of their config for use in making slightly different clients backed by the same
connection. For instance, given theclientabove, this is how to have a client in another namespace:config=client.config()config["namespace"]="my-other-namespace"other_ns_client=Client(**config)Data ConversionData converters are used to convert raw Temporal payloads to/from actual Python types. A custom data converter of typetemporalio.converter.DataConvertercan be set via thedata_converterclient parameter. Data converters are a
combination of payload converters, payload codecs, and failure converters. Payload converters convert Python values
to/from serialized bytes. Payload codecs convert bytes to bytes (e.g. for compression or encryption). Failure converters
convert exceptions to/from serialized failures.The default data converter supports converting multiple types including:Nonebytesgoogle.protobuf.message.Message- As JSON when encoding, but has ability to decode binary proto from other languagesAnything that can be converted to JSON including:Anything thatjson.dumpsupports nativelydataclassesIterables including ones JSON dump may not support by default, e.g.setAny class with adict()method and a staticparse_obj()method, e.g.Pydantic modelsThe default data converter is deprecated for Pydantic models and will warn if used since not all fields work.
Seethis samplefor the recommended
approach.IntEnum, StrEnumbased enumeratesUUIDThis notably doesn't include anydate,time, ordatetimeobjects as they may not work across SDKs.Users are strongly encouraged to use a singledataclassfor parameter and return types so fields with defaults can be
easily added without breaking compatibility.Classes with generics may not have the generics properly resolved. The current implementation does not have generic
type resolution. Users should use concrete types.Custom Type Data ConversionFor converting from JSON, the workflow/activity type hint is taken into account to convert to the proper type. Care has
been taken to support all common typings includingOptional,Union, all forms of iterables and mappings,NewType,
etc in addition to the regular JSON values mentioned before.Data converters contain a reference to a payload converter class that is used to convert to/from payloads/values. This
is a class and not an instance because it is instantiated on every workflow run inside the sandbox. The payload
converter is usually aCompositePayloadConverterwhich contains a multipleEncodingPayloadConverters it uses to try
to serialize/deserialize payloads. Upon serialization, eachEncodingPayloadConverteris tried until one succeeds. TheEncodingPayloadConverterprovides an "encoding" string serialized onto the payload so that, upon deserialization, the
specificEncodingPayloadConverterfor the given "encoding" is used.The default data converter uses theDefaultPayloadConverterwhich is simply aCompositePayloadConverterwith a known
set of defaultEncodingPayloadConverters. To implement a custom encoding for a custom type, a newEncodingPayloadConvertercan be created for the new type. For example, to supportIPv4Addresstypes:classIPv4AddressEncodingPayloadConverter(EncodingPayloadConverter):@propertydefencoding(self)->str:return"text/ipv4-address"defto_payload(self,value:Any)->Optional[Payload]:ifisinstance(value,ipaddress.IPv4Address):returnPayload(metadata={"encoding":self.encoding.encode()},data=str(value).encode(),)else:returnNonedeffrom_payload(self,payload:Payload,type_hint:Optional[Type]=None)->Any:assertnottype_hintortype_hintisipaddress.IPv4Addressreturnipaddress.IPv4Address(payload.data.decode())classIPv4AddressPayloadConverter(CompositePayloadConverter):def__init__(self)->None:# Just add ours as first before the defaultssuper().__init__(IPv4AddressEncodingPayloadConverter(),*DefaultPayloadConverter.default_encoding_payload_converters,)my_data_converter=dataclasses.replace(DataConverter.default,payload_converter_class=IPv4AddressPayloadConverter,)Imports are left off for brevity.This is good for many custom types. However, sometimes you want to override the behavior of the just the existing JSON
encoding payload converter to support a new type. It is already the last encoding data converter in the list, so it's
the fall-through behavior for any otherwise unknown type. Customizing the existing JSON converter has the benefit of
making the type work in lists, unions, etc.TheJSONPlainPayloadConverteruses the Pythonjsonlibrary with an
advanced JSON encoder by default and a custom value conversion method to turnjson.loaded values to their type hints.
The conversion can be customized for serialization with a customjson.JSONEncoderand deserialization with a customJSONTypeConverter. For example, to supportIPv4Addresstypes in existing JSON conversion:classIPv4AddressJSONEncoder(AdvancedJSONEncoder):defdefault(self,o:Any)->Any:ifisinstance(o,ipaddress.IPv4Address):returnstr(o)returnsuper().default(o)classIPv4AddressJSONTypeConverter(JSONTypeConverter):defto_typed_value(self,hint:Type,value:Any)->Union[Optional[Any],_JSONTypeConverterUnhandled]:ifissubclass(hint,ipaddress.IPv4Address):returnipaddress.IPv4Address(value)returnJSONTypeConverter.UnhandledclassIPv4AddressPayloadConverter(CompositePayloadConverter):def__init__(self)->None:# Replace default JSON plain with our own that has our encoder and type# converterjson_converter=JSONPlainPayloadConverter(encoder=IPv4AddressJSONEncoder,custom_type_converters=[IPv4AddressJSONTypeConverter()],)super().__init__(*[cifnotisinstance(c,JSONPlainPayloadConverter)elsejson_converterforcinDefaultPayloadConverter.default_encoding_payload_converters])my_data_converter=dataclasses.replace(DataConverter.default,payload_converter_class=IPv4AddressPayloadConverter,)NowIPv4Addresscan be used in type hints including collections, optionals, etc.WorkersWorkers host workflows and/or activities. Here's how to run a worker:importasyncioimportloggingfromtemporalio.clientimportClientfromtemporalio.workerimportWorker# Import your own workflows and activitiesfrommy_workflow_packageimportMyWorkflow,my_activityasyncdefrun_worker(stop_event:asyncio.Event):# Create client connected to server at the given addressclient=awaitClient.connect("localhost:7233",namespace="my-namespace")# Run the worker until the event is setworker=Worker(client,task_queue="my-task-queue",workflows=[MyWorkflow],activities=[my_activity])asyncwithworker:awaitstop_event.wait()Some things to note about the above code:This creates/uses the same client that is used for starting workflowsWhile this example accepts a stop event and usesasync with,run()andshutdown()may be used insteadWorkers can have many more options not shown here (e.g. data converters and interceptors)WorkflowsDefinitionWorkflows are defined as classes decorated with@workflow.defn. The method invoked for the workflow is decorated with@workflow.run. Methods for signals, queries, and updates are decorated with@workflow.signal,@workflow.queryand@workflow.updaterespectively. Here's an example of a workflow:importasynciofromdatetimeimporttimedeltafromtemporalioimportworkflow# Pass the activities through the sandboxwithworkflow.unsafe.imports_passed_through():from.my_activitiesimportGreetingInfo,create_greeting_activity@workflow.defnclassGreetingWorkflow:def__init__(self)->None:self._current_greeting="<unset>"self._greeting_info=GreetingInfo()self._greeting_info_update=asyncio.Event()self._complete=asyncio.Event()@workflow.runasyncdefrun(self,name:str)->str:self._greeting_info.name=namewhileTrue:# Store greetingself._current_greeting=awaitworkflow.execute_activity(create_greeting_activity,self._greeting_info,start_to_close_timeout=timedelta(seconds=5),)workflow.logger.debug("Greeting set to%s",self._current_greeting)# Wait for salutation update or complete signal (this can be# cancelled)awaitasyncio.wait([asyncio.create_task(self._greeting_info_update.wait()),asyncio.create_task(self._complete.wait()),],return_when=asyncio.FIRST_COMPLETED,)ifself._complete.is_set():returnself._current_greetingself._greeting_info_update.clear()@workflow.signalasyncdefupdate_salutation(self,salutation:str)->None:self._greeting_info.salutation=salutationself._greeting_info_update.set()@workflow.signalasyncdefcomplete_with_greeting(self)->None:self._complete.set()@workflow.querydefcurrent_greeting(self)->str:returnself._current_greeting@workflow.updatedefset_and_get_greeting(self,greeting:str)->str:old=self._current_greetingself._current_greeting=greetingreturnoldThis assumes there's an activity inmy_activities.pylike:fromdataclassesimportdataclassfromtemporalioimportworkflow@dataclassclassGreetingInfo:salutation:str="Hello"name:str="<unknown>"@activity.defndefcreate_greeting_activity(info:GreetingInfo)->str:returnf"{info.salutation},{info.name}!"Some things to note about the above workflow code:Workflows run in a sandbox by default.Users are encouraged to define workflows in files with no side effects or other complicated code or unnecessary
imports to other third party libraries.Non-standard-library, non-temporalioimports should usually be "passed through" the sandbox. See theWorkflow Sandboxsection for more details.This workflow continually updates the queryable current greeting when signalled and can complete with the greeting on
a different signalWorkflows are always classes and must have a single@workflow.runwhich is anasync deffunctionWorkflow code must be deterministic. This means no threading, no randomness, no external calls to processes, no
network IO, and no global state mutation. All code must run in the implicitasyncioevent loop and be deterministic.@activity.defnis explained in a later section. For normal simple string concatenation, this would just be done in
the workflow. The activity is for demonstration purposes only.workflow.execute_activity(create_greeting_activity, ...is actually a typed signature, and MyPy will fail if theself._greeting_infoparameter is not aGreetingInfoHere are the decorators that can be applied:@workflow.defn- Defines a workflow classMust be defined on the class given to the worker (ignored if present on a base class)Can have anameparam to customize the workflow name, otherwise it defaults to the unqualified class nameCan havedynamic=Truewhich means all otherwise unhandled workflows fall through to this. If present, cannot havenameargument, and run method must accept a single parameter ofSequence[temporalio.common.RawValue]type. The
payload of the raw value can be converted viaworkflow.payload_converter().from_payload.@workflow.run- Defines the primary workflow run methodMust be defined on the same class as@workflow.defn, not a base class (but canalsobe defined on the same
method of a base class)Exactly one method name must have this decorator, no more or lessMust be defined on anasync defmethodThe method's arguments are the workflow's argumentsThe first parameter must beself, followed by positional arguments. Best practice is to only take a single
argument that is an object/dataclass of fields that can be added to as needed.@workflow.signal- Defines a method as a signalCan be defined on anasyncor non-asyncfunction at any hierarchy depth, but if decorated method is overridden,
the override must also be decoratedThe method's arguments are the signal's argumentsCan have anameparam to customize the signal name, otherwise it defaults to the unqualified method nameCan havedynamic=Truewhich means all otherwise unhandled signals fall through to this. If present, cannot havenameargument, and method parameters must beself, a string signal name, and aSequence[temporalio.common.RawValue].Non-dynamic method can only have positional arguments. Best practice is to only take a single argument that is an
object/dataclass of fields that can be added to as needed.Return value is ignored@workflow.query- Defines a method as a queryAll the same constraints as@workflow.signalbut should return a valueShould not beasyncTemporal queries should never mutate anything in the workflow or call any calls that would mutate the workflow@workflow.update- Defines a method as an updateMay both accept as input and return a valueMay beasyncor non-asyncMay mutate workflow state, and make calls to other workflow APIs like starting activities, etc.Also accepts thenameanddynamicparameters like signals and queries, with the same semantics.Update handlers may optionally define a validator method by decorating it with@update_handler_method.validator.
To reject an update before any events are written to history, throw an exception in a validator. Validators cannot
beasync, cannot mutate workflow state, and return nothing.RunningTo start a locally-defined workflow from a client, you can simply reference its method like so:fromtemporalio.clientimportClientfrommy_workflow_packageimportGreetingWorkflowasyncdefcreate_greeting(client:Client)->str:# Start the workflowhandle=awaitclient.start_workflow(GreetingWorkflow.run,"my name",id="my-workflow-id",task_queue="my-task-queue")# Change the salutationawaithandle.signal(GreetingWorkflow.update_salutation,"Aloha")# Tell it to completeawaithandle.signal(GreetingWorkflow.complete_with_greeting)# Wait and return resultreturnawaithandle.result()Some things to note about the above code:This uses theGreetingWorkflowfrom the previous sectionThe result of calling this function is"Aloha, my name!"idandtask_queueare required for running a workflowclient.start_workflowis typed, so MyPy would fail if"my name"were something besides a stringhandle.signalis typed, so MyPy would fail if"Aloha"were something besides a string or if we provided a
parameter to the parameterlesscomplete_with_greetinghandle.resultis typed to the workflow itself, so MyPy would fail if we said thiscreate_greetingreturned
something besides a stringInvoking ActivitiesActivities are started with non-asyncworkflow.start_activity()which accepts either an activity function reference
or a string name.A single argument to the activity is positional. Multiple arguments are not supported in the type-safe form of
start/execute activity and must be supplied via theargskeyword argument.Activity options are set as keyword arguments after the activity arguments. At least one ofstart_to_close_timeoutorschedule_to_close_timeoutmust be provided.The result is an activity handle which is anasyncio.Taskand supports basic task featuresAn asyncworkflow.execute_activity()helper is provided which takes the same arguments asworkflow.start_activity()andawaits on the result. This should be used in most cases unless advanced task
capabilities are needed.Local activities work very similarly except the functions areworkflow.start_local_activity()andworkflow.execute_local_activity()⚠️Local activities are currently experimentalActivities can be methods of a class. Invokers should useworkflow.start_activity_method(),workflow.execute_activity_method(),workflow.start_local_activity_method(), andworkflow.execute_local_activity_method()instead.Activities can callable classes (i.e. that define__call__). Invokers should useworkflow.start_activity_class(),workflow.execute_activity_class(),workflow.start_local_activity_class(), andworkflow.execute_local_activity_class()instead.Invoking Child WorkflowsChild workflows are started with asyncworkflow.start_child_workflow()which accepts either a workflow run method
reference or a string name. The arguments to the workflow are positional.A single argument to the child workflow is positional. Multiple arguments are not supported in the type-safe form of
start/execute child workflow and must be supplied via theargskeyword argument.Child workflow options are set as keyword arguments after the arguments. At leastidmust be provided.Theawaitof the start does not complete until the start has been accepted by the serverThe result is a child workflow handle which is anasyncio.Taskand supports basic task features. The handle also has
some child info and supports signalling the child workflowAn asyncworkflow.execute_child_workflow()helper is provided which takes the same arguments asworkflow.start_child_workflow()andawaits on the result. This should be used in most cases unless advanced task
capabilities are needed.TimersA timer is represented by normalasyncio.sleep()Timers are also implicitly started on anyasynciocalls with timeouts (e.g.asyncio.wait_for)Timers are Temporal server timers, not local ones, so sub-second resolution rarely has valueCalls that use a specific point in time, e.g.call_atortimeout_at, should be based on the current loop time
(i.e.workflow.time()) and not an actual point in time. This is because fixed times are translated to relative ones
by subtracting the current loop time which may not be the actual current time.Conditionsworkflow.wait_conditionis an async function that doesn't return until a provided callback returns trueAtimeoutcan optionally be provided which will throw aasyncio.TimeoutErrorif reached (internally backed byasyncio.wait_forwhich uses a timer)Asyncio and CancellationWorkflows are backed by a customasyncioevent loop. This means many
of the commonasynciocalls work as normal. Some asyncio features are disabled such as:Thread related calls such asto_thread(),run_coroutine_threadsafe(),loop.run_in_executor(), etcCalls that alter the event loop such asloop.close(),loop.stop(),loop.run_forever(),loop.set_task_factory(), etcCalls that use anything external such as networking, subprocesses, disk IO, etcCancellation is done the same way asasyncio. Specifically, a task can be requested to be cancelled but does not
necessarily have to respect that cancellation immediately. This also means thatasyncio.shield()can be used to
protect against cancellation. The following tasks, when cancelled, perform a Temporal cancellation:Activities - when the task executing an activity is cancelled, a cancellation request is sent to the activityChild workflows - when the task starting or executing a child workflow is cancelled, a cancellation request is sent to
cancel the child workflowTimers - when the task executing a timer is cancelled (whether started via sleep or timeout), the timer is cancelledWhen the workflow itself is requested to cancel,Task.cancelis called on the main workflow task. Therefore,asyncio.CancelledErrorcan be caught in order to handle the cancel gracefully.Workflows followasynciocancellation rules exactly which can cause confusion among Python developers. Cancelling a
task doesn't always cancel the thing it created. For example, giventask = asyncio.create_task(workflow.start_child_workflow(..., callingtask.canceldoes not cancel the child
workflow, it only cancels the starting of it, which has no effect if it has already started. However, cancelling the
result ofhandle = await workflow.start_child_workflow(...ortask = asyncio.create_task(workflow.execute_child_workflow(...doescancel the child workflow.Also, due to Temporal rules, a cancellation request is a state not an event. Therefore, repeated cancellation requests
are not delivered, only the first. If the workflow chooses swallow a cancellation, it cannot be requested again.Workflow UtilitiesWhile running in a workflow, in addition to features documented elsewhere, the following items are available from thetemporalio.workflowpackage:continue_as_new()- Async function to stop the workflow immediately and continue as newinfo()- Returns information about the current workflowlogger- A logger for use in a workflow (properly skips logging on replay)now()- Returns the "current time" from the workflow's perspectiveExceptionsWorkflows can raise exceptions to fail the workflow or the "workflow task" (i.e. suspend the workflow retrying).Exceptions that are instances oftemporalio.exceptions.FailureErrorwill fail the workflow with that exceptionFor failing the workflow explicitly with a user exception, usetemporalio.exceptions.ApplicationError. This can
be marked non-retryable or include details as needed.Other exceptions that come from activity execution, child execution, cancellation, etc are already instances ofFailureErrorand will fail the workflow when uncaught.All other exceptions fail the "workflow task" which means the workflow will continually retry until the workflow is
fixed. This is helpful for bad code or other non-predictable exceptions. To actually fail the workflow, use anApplicationErroras mentioned above.External Workflowsworkflow.get_external_workflow_handle()inside a workflow returns a handle to interact with another workflowworkflow.get_external_workflow_handle_for()can be used instead for a type safe handleawait handle.signal()can be called on the handle to signal the external workflowawait handle.cancel()can be called on the handle to send a cancel to the external workflowTestingWorkflow testing can be done in an integration-test fashion against a real server, however it is hard to simulate
timeouts and other long time-based code. Using the time-skipping workflow test environment can help there.The time-skippingtemporalio.testing.WorkflowEnvironmentcan be created via the static asyncstart_time_skipping().
This internally downloads the Temporal time-skipping test server to a temporary directory if it doesn't already exist,
then starts the test server which has special APIs for skipping time.NOTE:The time-skipping test environment does not work on ARM. The SDK will try to download the x64 binary on macOS
for use with the Intel emulator, but for Linux or Windows ARM there is no proper time-skipping test server at this time.Automatic Time SkippingAnytime a workflow result is waited on, the time-skipping server automatically advances to the next event it can. To
manually advance time before waiting on the result of a workflow, theWorkflowEnvironment.sleepmethod can be used.Here's a simple example of a workflow that sleeps for 24 hours:importasynciofromtemporalioimportworkflow@workflow.defnclassWaitADayWorkflow:@workflow.runasyncdefrun(self)->str:awaitasyncio.sleep(24*60*60)return"all done"An integration test of this workflow would be way too slow. However the time-skipping server automatically skips to the
next event when we wait on the result. Here's a test for that workflow:fromtemporalio.testingimportWorkflowEnvironmentfromtemporalio.workerimportWorkerasyncdeftest_wait_a_day_workflow():asyncwithawaitWorkflowEnvironment.start_time_skipping()asenv:asyncwithWorker(env.client,task_queue="tq1",workflows=[WaitADayWorkflow]):assert"all done"==awaitenv.client.execute_workflow(WaitADayWorkflow.run,id="wf1",task_queue="tq1")That test will run almost instantly. This is because by callingexecute_workflowon our client, we have asked the
environment to automatically skip time as much as it can (basically until the end of the workflow or until an activity
is run).To disable automatic time-skipping while waiting for a workflow result, run code inside awith env.auto_time_skipping_disabled():block.Manual Time SkippingUntil a workflow is waited on, all time skipping in the time-skipping environment is done manually viaWorkflowEnvironment.sleep.Here's workflow that waits for a signal or times out:importasynciofromtemporalioimportworkflow@workflow.defnclassSignalWorkflow:def__init__(self)->None:self.signal_received=False@workflow.runasyncdefrun(self)->str:# Wait for signal or timeout in 45 secondstry:awaitworkflow.wait_condition(lambda:self.signal_received,timeout=45)return"got signal"exceptasyncio.TimeoutError:return"got timeout"@workflow.signaldefsome_signal(self)->None:self.signal_received=TrueTo test a normal signal, you might:fromtemporalio.testingimportWorkflowEnvironmentfromtemporalio.workerimportWorkerasyncdeftest_signal_workflow():asyncwithawaitWorkflowEnvironment.start_time_skipping()asenv:asyncwithWorker(env.client,task_queue="tq1",workflows=[SignalWorkflow]):# Start workflow, send signal, check resulthandle=awaitenv.client.start_workflow(SignalWorkflow.run,id="wf1",task_queue="tq1")awaithandle.signal(SignalWorkflow.some_signal)assert"got signal"==awaithandle.result()But how would you test the timeout part? Like so:fromtemporalio.testingimportWorkflowEnvironmentfromtemporalio.workerimportWorkerasyncdeftest_signal_workflow_timeout():asyncwithawaitWorkflowEnvironment.start_time_skipping()asenv:asyncwithWorker(env.client,task_queue="tq1",workflows=[SignalWorkflow]):# Start workflow, advance time past timeout, check resulthandle=awaitenv.client.start_workflow(SignalWorkflow.run,id="wf1",task_queue="tq1")awaitenv.sleep(50)assert"got timeout"==awaithandle.result()Also, the current time of the workflow environment can be obtained via the asyncWorkflowEnvironment.get_current_timemethod.Mocking ActivitiesActivities are just functions decorated with@activity.defn. Simply write different ones and pass those to the worker
to have different activities called during the test.Workflow SandboxBy default workflows are run in a sandbox to help avoid non-deterministic code. If a call that is known to be
non-deterministic is performed, an exception will be thrown in the workflow which will "fail the task" which means the
workflow will not progress until fixed.The sandbox is not foolproof and non-determinism can still occur. It is simply a best-effort way to catch bad code
early. Users are encouraged to define their workflows in files with no other side effects.The sandbox offers a mechanism to pass through modules from outside the sandbox. By default this already includes all
standard library modules and Temporal modules.For performance and behavior reasons, users are encouraged to pass
through all third party modules whose calls will be deterministic.This includes modules containing the activities to
be referenced in workflows. See "Passthrough Modules" below on how to do this.If you are getting an error like:temporalio.worker.workflow_sandbox._restrictions.RestrictedWorkflowAccessError: Cannot access
http.client.IncompleteRead.__mro_entries__ from inside a workflow. If this is code from a module not used in a
workflow or known to only be used deterministically from a workflow, mark the import as pass through.Then you are either using an invalid construct from the workflow, this is a known limitation of the sandbox, or most
commonly this is from a module that is safe to pass through (see "Passthrough Modules" section below).How the Sandbox WorksThe sandbox is made up of two components that work closely together:Global state isolationRestrictions preventing known non-deterministic library callsGlobal state isolation is performed by usingexec. Upon workflow start, the file that the workflow is defined in is
imported into a new sandbox created for that workflow run. In order to keep the sandbox performant a known set of
"passthrough modules" are passed through from outside of the sandbox when they are imported. These are expected to be
side-effect free on import and have their non-deterministic aspects restricted. By default the entire Python standard
library,temporalio, and a couple of other modules are passed through from outside of the sandbox. To update this
list, see "Customizing the Sandbox".Restrictions preventing known non-deterministic library calls are achieved using proxy objects on modules wrapped around
the custom importer set in the sandbox. Many restrictions apply at workflow import time and workflow run time, while
some restrictions only apply at workflow run time. A default set of restrictions is included that prevents most
dangerous standard library calls. However it is known in Python that some otherwise-non-deterministic invocations, like
reading a file from disk viaopenor usingos.environ, are done as part of importing modules. To customize what is
and isn't restricted, see "Customizing the Sandbox".Avoiding the SandboxThere are three increasingly-scoped ways to avoid the sandbox. Users are discouraged from avoiding the sandbox if
possible.To remove restrictions around a particular block of code, usewith temporalio.workflow.unsafe.sandbox_unrestricted():.
The workflow will still be running in the sandbox, but no restrictions for invalid library calls will be applied.To run an entire workflow outside of a sandbox, setsandboxed=Falseon the@workflow.defndecorator when defining
it. This will run the entire workflow outside of the workflow which means it can share global state and other bad
things.To disable the sandbox entirely for a worker, set theWorkerinit'sworkflow_runnerkeyword argument totemporalio.worker.UnsandboxedWorkflowRunner(). This value is defaulted totemporalio.worker.workflow_sandbox.SandboxedWorkflowRunner()so by changing it to the unsandboxed runner, the sandbox
will not be used at all.Customizing the Sandbox⚠️ WARNING: APIs in thetemporalio.worker.workflow_sandboxmodule are not yet considered stable and may change in
future releases.When creating theWorker, theworkflow_runneris defaulted totemporalio.worker.workflow_sandbox.SandboxedWorkflowRunner(). TheSandboxedWorkflowRunner's init accepts arestrictionskeyword argument that is defaulted toSandboxRestrictions.default. TheSandboxRestrictionsdataclass
is immutable and contains three fields that can be customized, but only two have notable value. See below.Passthrough ModulesBy default the sandbox completely reloads non-standard-library and non-Temporal modules for every workflow run. To make
the sandbox quicker and use less memory when importing known-side-effect-free third party modules, they can be marked
as passthrough modules.For performance and behavior reasons, users are encouraged to pass through all third party modules whose calls will be
deterministic.One way to pass through a module is at import time in the workflow file using theimports_passed_throughcontext
manager like so:# my_workflow_file.pyfromtemporalioimportworkflowwithworkflow.unsafe.imports_passed_through():importpydantic@workflow.defnclassMyWorkflow:...Alternatively, this can be done at worker creation time by customizing the runner's restrictions. For example:my_worker=Worker(...,workflow_runner=SandboxedWorkflowRunner(restrictions=SandboxRestrictions.default.with_passthrough_modules("pydantic")))In both of these cases, now thepydanticmodule will be passed through from outside of the sandbox instead of
being reloaded for every workflow run.Invalid Module MembersSandboxRestrictions.invalid_module_memberscontains a root matcher that applies to all module members. This already
has a default set which includes things likedatetime.date.today()which should never be called from a workflow. To
remove this restriction:my_restrictions=dataclasses.replace(SandboxRestrictions.default,invalid_module_members=SandboxRestrictions.invalid_module_members_default.with_child_unrestricted("datetime","date","today",),)my_worker=Worker(...,workflow_runner=SandboxedWorkflowRunner(restrictions=my_restrictions))Restrictions can also be added by|'ing together matchers, for example to restrict thedatetime.dateclass from
being used altogether:my_restrictions=dataclasses.replace(SandboxRestrictions.default,invalid_module_members=SandboxRestrictions.invalid_module_members_default|SandboxMatcher(children={"datetime":SandboxMatcher(use={"date"})},),)my_worker=Worker(...,workflow_runner=SandboxedWorkflowRunner(restrictions=my_restrictions))See the API for more details on exact fields and their meaning.Known Sandbox IssuesBelow are known sandbox issues. As the sandbox is developed and matures, some may be resolved.Global Import/BuiltinsCurrently the sandbox references/alters the globalsys.modulesandbuiltinsfields while running workflow code. In
order to prevent affecting other sandboxed code, thread locals are leveraged to only intercept these values during the
workflow thread running. Therefore, technically if top-level import code starts a thread, it may lose sandbox
protection.Sandbox is not SecureThe sandbox is built to catch many non-deterministic and state sharing issues, but it is not secure. Some known bad
calls are intercepted, but for performance reasons, every single attribute get/set cannot be checked. Therefore a simple
call likesetattr(temporalio.common, "__my_key", "my value")will leak across sandbox runs.The sandbox is only a helper, it does not provide full protection.Sandbox PerformanceThe sandbox does not add significant CPU or memory overhead for workflows that are in files which only import standard
library modules. This is because they are passed through from outside of the sandbox. However, every
non-standard-library import that is performed at the top of the same file the workflow is in will add CPU overhead (the
module is re-imported every workflow run) and memory overhead (each module independently cached as part of the workflow
run for isolation reasons). This becomes more apparent for large numbers of workflow runs.To mitigate this, users should:Define workflows in files that have as few non-standard-library imports as possibleAlter the max workflow cache and/or max concurrent workflows settings if memory grows too largeSet third-party libraries as passthrough modules if they are known to be side-effect freeExtending Restricted ClassesExtending a restricted class causes Python to instantiate the restricted metaclass which is unsupported. Therefore if
you attempt to use a class in the sandbox that extends a restricted class, it will fail. For example, if you have aclass MyZipFile(zipfile.ZipFile)and try to use that class inside a workflow, it will fail.Classes used inside the workflow should not extend restricted classes. For situations where third-party modules need to
at import time, they should be marked as pass through modules.Certain Standard Library Calls on Restricted ObjectsIf an object is restricted, internal C Python validation may fail in some cases. For example, runningdict.items(os.__dict__)will fail with:descriptor 'items' for 'dict' objects doesn't apply to a '_RestrictedProxy' objectThis is a low-level check that cannot be subverted. The solution is to not use restricted objects inside the sandbox.
For situations where third-party modules need to at import time, they should be marked as pass through modules.is_subclass of ABC-based Restricted ClassesDue tohttps://bugs.python.org/issue44847, classes that are wrapped and then
checked to see if they are subclasses of another viais_subclassmay fail (see alsothis wrapt issue).Compiled Pydantic Sometimes Using Wrong TypesIf the Pydantic dependency is in compiled form (the default) and you are using a Pydantic model inside a workflow
sandbox that uses adatetimetype, it will grab the wrong validator and usedateinstead. This is because our
patched form ofissubclassis bypassed by compiled Pydantic.To work around, either don't usedatetime-based Pydantic model fields in workflows, or markdatetimelibrary as
passthrough (means you lose protection against calling the non-deterministicnow()), or use non-compiled Pydantic
dependency.ActivitiesDefinitionActivities are decorated with@activity.defnlike so:fromtemporalioimportactivity@activity.defndefsay_hello_activity(name:str)->str:returnf"Hello,{name}!"Some things to note about activity definitions:Thesay_hello_activityis synchronous which is the recommended activity type (see "Types of Activities" below), but
it can beasyncA custom name for the activity can be set with a decorator argument, e.g.@activity.defn(name="my activity")Long running activities should regularly heartbeat and handle cancellationActivities can only have positional arguments. Best practice is to only take a single argument that is an
object/dataclass of fields that can be added to as needed.Activities can be defined on methods instead of top-level functions. This allows the instance to carry state that an
activity may need (e.g. a DB connection). The instance method should be what is registered with the worker.Activities can also be defined on callable classes (i.e. classes with__call__). An instance of the class should be
what is registered with the worker.The@activity.defncan havedynamic=Trueset which means all otherwise unhandled activities fall through to this.
If present, cannot havenameargument, and the activity function must accept a single parameter ofSequence[temporalio.common.RawValue]. The payload of the raw value can be converted viaactivity.payload_converter().from_payload.Types of ActivitiesThere are 3 types of activity callables accepted and described below: synchronous multithreaded, synchronous
multiprocess/other, and asynchronous. Only positional parameters are allowed in activity callables.Synchronous ActivitiesSynchronous activities, i.e. functions that do not haveasync def, can be used with workers, but theactivity_executorworker parameter must be set with aconcurrent.futures.Executorinstance to use for executing the
activities.All long running, non-local activities should heartbeat so they can be cancelled. Cancellation in threaded activities
throws but multiprocess/other activities does not. The sections below on each synchronous type explain further. There
are also calls on the context that can check for cancellation. For more information, see "Activity Context" and
"Heartbeating and Cancellation" sections later.Note, all calls from an activity to functions in thetemporalio.activitypackage are powered bycontextvars. Therefore, new threads startinginsideof
activities mustcopy_context()and then.run()manually to ensuretemporalio.activitycalls likeheartbeatstill
function in the new threads.If any activity ever throws aconcurrent.futures.BrokenExecutor, the failure is consisted unrecoverable and the worker
will fail and shutdown.Synchronous Multithreaded ActivitiesIfactivity_executoris set to an instance ofconcurrent.futures.ThreadPoolExecutorthen the synchronous activities
are considered multithreaded activities. Ifmax_workersis not set to at least the worker'smax_concurrent_activitiessetting a warning will be issued. Besidesactivity_executor, no other worker parameters
are required for synchronous multithreaded activities.By default, cancellation of a synchronous multithreaded activity is done via atemporalio.exceptions.CancelledErrorthrown into the activity thread. Activities that do not wish to have cancellation thrown can setno_thread_cancel_exception=Truein the@activity.defndecorator.Code that wishes to be temporarily shielded from the cancellation exception can run insidewith activity.shield_thread_cancel_exception():. But once the last nested form of that block is finished, even if
there is a return statement within, it will throw the cancellation if there was one. Atry+except temporalio.exceptions.CancelledErrorwould have to surround thewithto handle the cancellation explicitly.Synchronous Multiprocess/Other ActivitiesIfactivity_executoris set to an instance ofconcurrent.futures.Executorthat isnotconcurrent.futures.ThreadPoolExecutor, then the synchronous activities are considered multiprocess/other activities.
Users should prefer threaded activities over multiprocess ones since, among other reasons, threaded activities can raise
on cancellation.These require special primitives for heartbeating and cancellation. Theshared_state_managerworker parameter must be
set to an instance oftemporalio.worker.SharedStateManager. The most common implementation can be created by passing amultiprocessing.managers.SyncManager(i.e. result ofmultiprocessing.managers.Manager()) totemporalio.worker.SharedStateManager.create_from_multiprocessing().Also, all of these activity functions must be"picklable".Asynchronous ActivitiesAsynchronous activities are functions defined withasync def. Asynchronous activities are often much more performant
than synchronous ones. When using asynchronous activities no special worker parameters are needed.⚠️ WARNING: Do not block the thread inasync defPython functions. This can stop the processing of the rest of the
Temporal.Cancellation for asynchronous activities is done viaasyncio.Task.cancel. This means thatasyncio.CancelledErrorwill be raised (and can be caught, but it is not recommended). A non-local activity must
heartbeat to receive cancellation and there are other ways to be notified about cancellation (see "Activity Context" and
"Heartbeating and Cancellation" later).Activity ContextDuring activity execution, an implicit activity context is set as acontext variable. The context variable itself is not visible, but
calls in thetemporalio.activitypackage make use of it. Specifically:in_activity()- Whether an activity context is presentinfo()- Returns the immutable info of the currently running activityheartbeat(*details)- Record a heartbeatis_cancelled()- Whether a cancellation has been requested on this activitywait_for_cancelled()-asynccall to wait for cancellation requestwait_for_cancelled_sync(timeout)- Synchronous blocking call to wait for cancellation requestshield_thread_cancel_exception()- Context manager for use inwithclauses by synchronous multithreaded activities
to prevent cancel exception from being thrown during the block of codeis_worker_shutdown()- Whether the worker has started graceful shutdownwait_for_worker_shutdown()-asynccall to wait for start of graceful worker shutdownwait_for_worker_shutdown_sync(timeout)- Synchronous blocking call to wait for start of graceful worker shutdownraise_complete_async()- Raise an error that this activity will be completed asynchronously (i.e. after return of
the activity function in a separate client call)With the exception ofin_activity(), if any of the functions are called outside of an activity context, an error
occurs. Synchronous activities cannot call any of theasyncfunctions.Heartbeating and CancellationIn order for a non-local activity to be notified of cancellation requests, it must be given aheartbeat_timeoutat
invocation time and invoketemporalio.activity.heartbeat()inside the activity. It is strongly recommended that all
but the fastest executing activities call this function regularly. "Types of Activities" has specifics on cancellation
for synchronous and asynchronous activities.In addition to obtaining cancellation information, heartbeats also support detail data that is persisted on the server
for retrieval during activity retry. If an activity callstemporalio.activity.heartbeat(123, 456)and then fails and
is retried,temporalio.activity.info().heartbeat_detailswill return an iterable containing123and456on the
next run.Heartbeating has no effect on local activities.Worker ShutdownAn activity can react to a worker shutdown. Usingis_worker_shutdownor one of thewait_for_worker_shutdownfunctions an activity can react to a shutdown.When thegraceful_shutdown_timeoutworker parameter is given adatetime.timedelta, on shutdown the worker will
notify activities of the graceful shutdown. Once that timeout has passed (or if wasn't set), the worker will perform
cancellation of all outstanding activities.Theshutdown()invocation will wait on all activities to complete, so if a long-running activity does not at least
respect cancellation, the shutdown may never complete.TestingUnit testing an activity or any code that could run in an activity is done via thetemporalio.testing.ActivityEnvironmentclass. Simply instantiate this and any callable + params passed torunwill
be invoked inside the activity context. The following are attributes/methods on the environment that can be used to
affect calls activity code might make to functions on thetemporalio.activitypackage.infoproperty can be set to customize what is returned fromactivity.info()on_heartbeatproperty can be set to handleactivity.heartbeat()callscancel()can be invoked to simulate a cancellation of the activityworker_shutdown()can be invoked to simulate a worker shutdown during execution of the activityWorkflow ReplayGiven a workflow's history, it can be replayed locally to check for things like non-determinism errors. For example,
assuminghistory_stris populated with a JSON string history either exported from the web UI or fromtctl, the
following function will replay it:fromtemporalio.clientimportWorkflowHistoryfromtemporalio.workerimportReplayerasyncdefrun_replayer(history_str:str):replayer=Replayer(workflows=[SayHello])awaitreplayer.replay_workflow(WorkflowHistory.from_json(history_str))This will throw an error if any non-determinism is detected.Replaying from workflow history is a powerful concept that many use to test that workflow alterations won't cause
non-determinisms with past-complete workflows. The following code will make sure that all workflow histories for a
certain workflow type (i.e. workflow class) are safe with the current code.fromtemporalio.clientimportClient,WorkflowHistoryfromtemporalio.workerimportReplayerasyncdefcheck_past_histories(my_client:Client):replayer=Replayer(workflows=[SayHello])awaitreplayer.replay_workflows(awaitmy_client.list_workflows("WorkflowType = 'SayHello'").map_histories(),)OpenTelemetry SupportOpenTelemetry support requires the optionalopentelemetrydependencies which are part of theopentelemetryextra.
When usingpip, runningpip install temporalio[opentelemetry]will install needed dependencies. Then thetemporalio.contrib.opentelemetry.TracingInterceptorcan be created and set
as an interceptor on theinterceptorsargument ofClient.connect. When set, spans will be created for all client
calls and for all activity and workflow invocations on the worker, spans will be created and properly serialized through
the server to give one proper trace for a workflow execution.Protobuf 3.x vs 4.xPython currently has two somewhat-incompatible protobuf library versions - the 3.x series and the 4.x series. Python
currently recommends 4.x and that is the primary supported version. Some libraries likePulumirequire 4.x. Other libraries such asONNXandStreamlit, for one reason or another, have/will not leave 3.x.To support these, Temporal Python SDK allows any protobuf library >= 3.19. However, the C extension in older Python
versions can cause issues with the sandbox due to global state sharing. Temporal strongly recommends using the latest
protobuf 4.x library unless you absolutely cannot at which point some proto libraries may have to be marked asPassthrough Modules.Known Compatibility IssuesBelow are known compatibility issues with the Python SDK.gevent PatchingWhen usinggevent.monkey.patch_all(), asyncio event loops can get messed up, especially those using custom event loops
like Temporal. Seethis gevent issue. This is a known incompatibility and
users are encouraged to not use gevent in asyncio applications (including Temporal). But if you must, there isa sampleshowing how it is possible.DevelopmentThe Python SDK is built to work with Python 3.8 and newer. It is built usingSDK Corewhich is written in Rust.BuildingPrepareTo build the SDK from source for use as a dependency, the following prerequisites are required:Python>= 3.8Make sure the latest version ofpipis in useRustpoetry(e.g.python -m pip install poetry)poe(e.g.python -m pip install poethepoet)macOS note: If errors are encountered, it may be better to install Python and Rust as recommended from their websites
instead of viabrew.With the prerequisites installed, first clone the SDK repository recursively:gitclone--recursivehttps://github.com/temporalio/sdk-python.gitcdsdk-pythonUsepoetryto install the dependencies with--no-rootto not install this package (because we still need to build
it):poetryinstall--no-root--all-extrasBuildNow perform the release build:This will take a while because Rust will compile the core project in release mode (seeLocal SDK development
environmentfor the quicker approach to local development).poetrybuildThe compiled wheel doesn't have the exact right tags yet for use, so run this script to fix it:poefix-wheelThewhlwheel file indist/is now ready to use.UseThe wheel can now be installed into any virtual environment.For example,create a virtual environmentsomewhere and then run the following inside the virtual environment:pipinstallwheelpipinstall/path/to/cloned/sdk-python/dist/*.whlCreate this Python file atexample.py:importasynciofromtemporalioimportworkflow,activityfromtemporalio.clientimportClientfromtemporalio.workerimportWorker@workflow.defnclassSayHello:@workflow.runasyncdefrun(self,name:str)->str:returnf"Hello,{name}!"asyncdefmain():client=awaitClient.connect("localhost:7233")asyncwithWorker(client,task_queue="my-task-queue",workflows=[SayHello]):result=awaitclient.execute_workflow(SayHello.run,"Temporal",id="my-workflow-id",task_queue="my-task-queue")print(f"Result:{result}")if__name__=="__main__":asyncio.run(main())Assuming there is alocal Temporal serverrunning, execute the
file withpython(orpython3if necessary):pythonexample.pyIt should output:Result: Hello, Temporal!Local SDK development environmentFor local development, it is often quicker to use debug builds and a local virtual environment.While not required, it often helps IDEs if we put the virtual environment.venvdirectory in the project itself. This
can be configured system-wide via:poetryconfigvirtualenvs.in-projecttrueNow perform the same steps as the "Prepare" section above by installing the prerequisites, cloning the project,
installing dependencies, and generating the protobuf code:gitclone--recursivehttps://github.com/temporalio/sdk-python.gitcdsdk-python
poetryinstall--no-root--all-extrasNow compile the Rust extension in develop mode which is quicker than release mode:poebuild-developThat step can be repeated for any Rust changes made.The environment is now ready to develop in.TestingTo execute tests:poetestThis runs againstTemporalite. To run against the time-skipping test
server, pass--workflow-environment time-skipping. To run against thedefaultnamespace of an already-running
server, pass thehost:portto--workflow-environment. Can also use regular pytest arguments. For example, here's how
to run a single test with debug logs on the console:poetest-s--log-cli-level=DEBUG-ktest_sync_activity_thread_cancel_caughtProto Generation and TestingTo allow for backwards compatibility, protobuf code is generated on the 3.x series of the protobuf library. To generate
protobuf code, you must be on Python <= 3.10, and then runpoetry add "protobuf<4". Then the protobuf files can be
generated viapoe gen-protos. Tests can be run for protobuf version 3 by setting theTEMPORAL_TEST_PROTO3env var
to1prior to running tests.Do not commitpoetry.lockorpyproject.tomlchanges. To go back from this downgrade, restorepyproject.tomland
runpoetry update protobuf grpcio-tools.For a less system-intrusive approach, you can:dockerbuild-fscripts/_proto/Dockerfile.
dockerrun-v"${PWD}/temporalio/api:/api_new"-v"${PWD}/temporalio/bridge/proto:/bridge_new"<justbuiltimagesha>
poeformatStyleMostlyGoogle Style Guide. Notable exceptions:We useBlackfor formatting, so that takes precedenceIn tests and example code, can import individual classes/functions to make it more readable. Can also do this for
rarely in library code for some Python common items (e.g.dataclassorpartial), but not allowed to do this for
anytemporaliopackages (excepttemporalio.types) or any classes/functions that aren't clear when unqualified.We allow relative imports for private packagesWe allow@staticmethod