pyramid-temporal
pyramid-temporal provides automatic transaction management for Temporal activities using pyramid_tm, exactly how it works for web requests.
This library gives Temporal activities real Pyramid requests (built with Pyramid's request factory and request extensions), so all your existing request methods work automatically - request.dbsession, request.tm, and any other methods configured via add_request_method.
- Github repository: https://github.com/cartaorobbin/pyramid-temporal/
- Documentation https://cartaorobbin.github.io/pyramid-temporal/
Features
- Real Pyramid Requests: Activities get actual
pyramid.request.Requestobjects, not mocks - Automatic Transaction Management: Uses
pyramid_tm- same as web requests - Full Pyramid Integration: All
add_request_methodconfigurations work automatically - PyramidEnvironment: Clean wrapper for bootstrap environment with access to app, registry, root
- Unit of Work Pattern: Each execution runs in its own transactional scope with a fresh request
- Safe Concurrency: Nothing is shared between executions, so
max_concurrent_activitiescan be raised - Async or Sync Activities: Write
async deffor cooperative work, or plaindefto run blocking work in Temporal's activity thread pool - Clean Activity Code: No manual transaction handling or context setup needed
- Custom Worker:
pyramid_temporal.Workerhandles activity binding automatically
Quick Start
Installation
pip install pyramid-temporal
Basic Usage
from temporalio import workflow
from temporalio.client import Client
from pyramid_temporal import Worker, activity, ActivityContext, PyramidEnvironment
# Define activities with automatic context injection
@activity.defn
async def enrich_user(context: ActivityContext, user_id: int) -> bool:
"""Activity with full Pyramid integration.
context.request is a REAL Pyramid Request object with all
configured request methods (dbsession, tm, etc.) available.
"""
# Access database session - transactions are automatic!
session = context.request.dbsession
user = session.query(User).get(user_id)
if user:
user.enriched = True
# No need to commit - pyramid_tm handles it on success
return True
return False
@activity.defn
async def send_notification(context: ActivityContext, user_id: int, message: str) -> bool:
"""Another activity using context."""
# Access settings from the real request
api_key = context.request.registry.settings.get('notification.api_key')
# ... send notification
return True
@workflow.defn(sandboxed=False)
class UserOnboardingWorkflow:
@workflow.run
async def run(self, user_id: int) -> bool:
# Enrich user data
await workflow.execute_activity(
enrich_user, user_id,
schedule_to_close_timeout=timedelta(seconds=60)
)
# Send welcome notification
await workflow.execute_activity(
send_notification, user_id, "Welcome!",
schedule_to_close_timeout=timedelta(seconds=60)
)
return True
Blocking Activities
Write the activity as a plain def whenever its body blocks - synchronous
database work, HTTP calls, or gRPC calls. Temporal runs sync activities in the
worker's activity thread pool, so a slow or stuck call never occupies the event
loop and never delays the other activities the worker is running:
@activity.defn
def import_orders(context: ActivityContext, batch_id: int) -> int:
"""Blocking body: runs in the activity thread pool, not on the event loop."""
session = context.request.dbsession
orders = provider_client.fetch(batch_id) # blocking gRPC/HTTP is fine here
session.add_all(orders)
return len(orders)
An async def activity still runs on the worker's event loop, so it must only
block cooperatively (await). Both flavours receive the same ActivityContext.
Worker Setup
from pyramid_temporal import Worker, PyramidEnvironment
def create_worker(env: PyramidEnvironment):
"""Create worker with Pyramid integration.
Args:
env: PyramidEnvironment from bootstrap (provided by CLI)
"""
client = env.registry.get('temporal_client')
# Worker automatically binds activities to the environment
worker = Worker(
client,
env, # Full Pyramid environment
task_queue="my-queue",
activities=[enrich_user, send_notification, import_orders],
workflows=[UserOnboardingWorkflow],
max_concurrent_activities=10,
)
return worker
Concurrency
Every activity execution owns its ActivityContext, its Pyramid request, and
therefore its own dbsession and tm. Executions share nothing, so
max_concurrent_activities can be set as high as your database pool allows.
Two things are worth checking in the application configuration:
- Use an explicit transaction manager: set
tm.manager_hook = pyramid_tm.explicit_managerin your settings. Without it,request.tmfalls back to the process-widetransaction.manager, which concurrent async executions would share on the event loop thread. - Size the database pool for the number of activity slots, since each concurrent execution checks out its own connection.
For sync activities the worker creates a ThreadPoolExecutor sized to
max_concurrent_activities (Temporal's default of 100 when unset). Pass your own
activity_executor= to control it yourself.
Pyramid Configuration
In your Pyramid application, configure as you normally would for web requests:
from pyramid.config import Configurator
def main(global_config, **settings):
config = Configurator(settings=settings)
# Standard Pyramid/pyramid_tm setup
config.include('pyramid_tm')
config.include('pyramid_temporal')
# Configure request.dbsession as you normally would
config.add_request_method(
lambda r: get_tm_session(session_factory, r.tm),
'dbsession',
reify=True
)
return config.make_wsgi_app()
The same configuration works for both web requests and Temporal activities!
Starting and Signaling Workflows from Sync Code
Pyramid views and event subscribers run synchronously, but the Temporal client is async.
pyramid-temporal hides the sync->async bridge so you never re-implement Client.connect +
asyncio.run yourself.
In a view or subscriber, use the request methods (they read connection settings automatically):
def create_reversal_view(request):
# ... build workflow_input ...
run_id = request.temporal_start_workflow(
ReversalWorkflow.run,
workflow_input,
id=f"reversal-{reversal_id}",
)
# signal a running workflow
request.temporal_signal_workflow(workflow_id, run_id, "provider_return_received")
In request-free code (e.g. a CLI command), use the module-level functions:
from pyramid_temporal import start_workflow, signal_workflow
run_id = start_workflow(
temporal_host="localhost:7233",
namespace="default",
task_queue="payments",
workflow_run=CreateChargeWorkflow.run,
arg=workflow_input,
id=workflow_id,
)
# Pass wait=True to block until the workflow finishes and get its result instead:
result = start_workflow(
temporal_host="localhost:7233",
namespace="default",
task_queue="payments",
workflow_run=CreateChargeWorkflow.run,
arg=workflow_input,
id=workflow_id,
wait=True,
)
Connection settings for the request methods come from the registry:
pyramid_temporal.temporal_host(defaultlocalhost:7233)pyramid_temporal.temporal_namespace(defaultdefault); the aliaspyramid_temporal.namespaceis also accepted and takes precedencepyramid_temporal.task_queue(defaultdefault); override per call with thetask_queue=keyword
CLI Usage
Start workers using the CLI command:
ptemporal-worker development.ini myapp.workers.create_worker
How It Works
Registering an activity binds it to the Pyramid environment. Every call of that bound activity is one execution, and one execution is one unit of work:
- Bootstrap →
PyramidEnvironmentwraps the full Pyramid bootstrap (app, registry, root) - Activity Starts → A fresh
ActivityContextbuilds a real Pyramid Request with Pyramid's request factory, then applies youradd_request_methodextensions - Transaction Begins → Using that request's own
request.tm - Context Injected → The activity body receives the context and reads
context.request - Activity Succeeds → Transaction commits automatically (via
pyramid_tm) - Activity Fails → Transaction aborts automatically
- Cleanup → Finished callbacks run and the request is closed
This is exactly how pyramid_tm works for web requests - your activities use the same patterns.
Pyramid threadlocals
context.request is the supported way to reach the request, and it works in
every activity. pyramid.threadlocal.get_current_request() depends on the
flavour, because Pyramid's threadlocal stack lives on the thread:
| Activity | get_current_request() |
get_current_registry() |
|---|---|---|
Sync (def) |
this execution's request | the application registry |
Async (async def) |
None |
the application registry |
A sync activity owns its thread in the activity executor, so its request can be published there safely. Concurrent async executions share the worker's event loop thread, where a per-execution request cannot be isolated, so the registry is published alone rather than handing out another execution's request.
API Reference
@activity.defn
Decorator to define a pyramid-temporal activity with context injection. Works on
async def and on plain def:
@activity.defn
async def my_activity(context: ActivityContext, arg1: str, arg2: int) -> bool:
session = context.request.dbsession
# ...
@activity.defn
def my_blocking_activity(context: ActivityContext, arg1: str) -> bool:
session = context.request.dbsession
# ... blocking work, run in the activity thread pool ...
Keyword arguments:
name=- register under a custom activity name (defaults to the function name)no_thread_cancel_exception=- for sync activities, skip raising the cancellation exception inside the activity thread
PyramidEnvironment
Wrapper for the Pyramid bootstrap environment:
from pyramid.paster import bootstrap
from pyramid_temporal import PyramidEnvironment
# Create from bootstrap output
env = PyramidEnvironment.from_bootstrap(bootstrap('development.ini'))
# Access components
env.registry # Pyramid registry
env.app # WSGI application
env.request # Base request object
env.root # Root object (for traversal)
env.settings # Shortcut to registry.settings
# Clean up when done
env.close()
ActivityContext
Context object passed to activities. One context belongs to one execution, and
accessing context.request outside of an execution raises RuntimeError:
context.env- FullPyramidEnvironmentcontext.registry- Pyramid registry (shortcut toenv.registry)context.settings- Application settings (shortcut toenv.settings)context.request- Real Pyramid Request with all configured methods:request.dbsession- if configured viaadd_request_methodrequest.tm- ifpyramid_tmis included- Any other methods you've configured
Worker
Pyramid-aware Temporal worker. Any additional keyword argument is passed
straight to temporalio.worker.Worker:
worker = Worker(
client, # Temporal client
env, # PyramidEnvironment (required)
task_queue="...", # Task queue name
activities=[...], # List of activities
workflows=[...], # List of workflows
max_concurrent_activities=10, # Safe to raise above 1
)
worker.env- thePyramidEnvironmentactivities are bound toworker.task_queue- the polled task queueworker.activity_executor- the thread pool the worker created for sync activities, orNonewhen it created none
Client helpers
Registered request methods (read connection settings from the registry):
request.temporal_start_workflow(workflow_run, arg, *, id, task_queue=None) -> run_id— defaults to the requiredpyramid_temporal.task_queuesetting; passtask_queue=to override for a single callrequest.temporal_signal_workflow(workflow_id, run_id, signal, *args) -> None
Request-free functions (for CLIs and scripts):
pyramid_temporal.start_workflow(*, temporal_host, namespace, task_queue, workflow_run, arg, id, wait=False) -> run_id— returns therun_id; passwait=Trueto block until the workflow completes and return its result insteadpyramid_temporal.signal_workflow(*, temporal_host, namespace, workflow_id, run_id, signal, args=()) -> None
Both run the async client on a dedicated worker thread, so they are safe to call from synchronous views, subscribers, and CLI commands.
Development
See .dev-local/README.md for development setup instructions.
Inspiration
This library is inspired by pyramid_tm, which provides excellent transaction management for Pyramid web applications. We apply the same pattern to Temporal activities.
Repository initiated with fpgmaas/cookiecutter-poetry.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyramid_temporal-0.1.1.tar.gz.
File metadata
- Download URL: pyramid_temporal-0.1.1.tar.gz
- Upload date:
- Size: 23.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01b7ffadde80d066cc57450442647b31c103e45836e444af69f3912f98bdec28
|
|
| MD5 |
474ed20d214a7fc0659ac901733ade60
|
|
| BLAKE2b-256 |
1bff8550a9dc22a3bada5c6c09841bf5f5510dab2182c5c499fb39b11ad9383f
|
File details
Details for the file pyramid_temporal-0.1.1-py3-none-any.whl.
File metadata
- Download URL: pyramid_temporal-0.1.1-py3-none-any.whl
- Upload date:
- Size: 24.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
896940285ccfbdd75ca2e08afd80743925b20344c3592d4f23794b05e71c6919
|
|
| MD5 |
ad03bc16d2439a8a835b3970d1955842
|
|
| BLAKE2b-256 |
17eb9405962499147fc582f8dee095d1b028f0fd56c9f72fc5ed91320e9c2c3e
|