question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Bug] SandboxWorkflowRunner doesn't use correct Pydantic field types in some cases

See original GitHub issue

What are you really trying to do?

I want to be able to pass in Pydantic models with datetime fields.

To encode datetimefields, I am using the pydantic.json.pydantic_encoder but can hit this behaviour without any data conversion occurring.

Describe the bug

The SandboxWorkflowRunner restrictions are resulting in Pydantic models being created with incorrect field types.

Specifically, I am seeing models derived from Pydantic’s BaseModel class (let’s say MyDatetimeWrapper) that contain a datetime field being instantiated with a date object instead when a MyDatetimeWrapper object is created within a workflow.

Minimal Reproduction

I’ve created a code sample to illustrate the issue:

import asyncio
import dataclasses
from datetime import datetime
from uuid import uuid4

from pydantic import BaseModel
from temporalio import workflow
from temporalio.client import Client
from temporalio.worker import Worker
from temporalio.worker.workflow_sandbox import SandboxRestrictions, SandboxedWorkflowRunner

class Message(BaseModel):
    content: datetime


@workflow.defn
class Workflow:

    @workflow.run
    async def run(self, context: str) -> None:
        message = Message(content=workflow.now())
        print(f"Message in workflow with {context}: {message} (`content` type is{type(message.content)})\n")


def default_worker(client: Client, task_queue: str) -> Worker:
    return Worker(client, workflows=[Workflow], activities=[], task_queue=task_queue)


def relaxed_worker(client: Client, task_queue: str) -> Worker:
    restrictions = dataclasses.replace(
        SandboxRestrictions.default,
        invalid_module_members=dataclasses.replace(
            SandboxRestrictions.invalid_module_members_default,
            children={
                k: v for k, v in SandboxRestrictions.invalid_module_members_default.children.items() if k != "datetime"
            }
        )
    )
    runner = SandboxedWorkflowRunner(restrictions=restrictions)
    return Worker(client, workflows=[Workflow], activities=[], task_queue=task_queue, workflow_runner=runner)


async def main():
    task_queue = str(uuid4())
    client = await Client.connect("temporal:7233", namespace="default-namespace")

    async with default_worker(client, task_queue):
        context = "default sandbox"
        message = Message(content=datetime.utcnow())
        print(f"Message in main with {context}: {message} (`content` type is{type(message.content)})\n")
        handle = await client.start_workflow(Workflow.run, context, id=str(uuid4()), task_queue=task_queue)
        await handle.result()


    async with relaxed_worker(client, task_queue):
        context = "relaxed sandbox"
        message = Message(content=datetime.utcnow())
        print(f"Message in main with {context}: {message} (`content` type is{type(message.content)})\n")
        handle = await client.start_workflow(Workflow.run, context, id=str(uuid4()), task_queue=task_queue)
        await handle.result()


if __name__ == "__main__":
    asyncio.run(main())

The output from this snippet it:

Message in main with default sandbox: content=datetime.datetime(2022, 11, 18, 14, 13, 25, 326269) (`content` type is<class 'datetime.datetime'>)

Message in workflow with default sandbox: content=datetime.date(2022, 11, 18) (`content` type is<class 'datetime.date'>)

Message in main with relaxed sandbox: content=datetime.datetime(2022, 11, 18, 14, 13, 25, 360676) (`content` type is<class 'datetime.datetime'>)

Message in workflow with relaxed sandbox: content=datetime.datetime(2022, 11, 18, 14, 13, 25, 369645, tzinfo=datetime.timezone.utc) (`content` type is<class 'datetime.datetime'>)

As you can see, the default sandbox runner is resulting in content being converted to just a date despite being instantiated with a datetime.

Environment/Versions

  • OS and processor: M1 Pro Mac
  • Temporal Version: 1.8.4 and SDK version 0.1b3 (Pydantic 1.10.2)
  • Using Docker to run Temporal server
  • Python Version: Python 3.10.7

Issue Analytics

  • State:open
  • Created 10 months ago
  • Comments:9 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
cretzcommented, Nov 18, 2022

Here’s my small reproduction:

import asyncio
from datetime import datetime
from uuid import uuid4

from pydantic import BaseModel
from temporalio import workflow
from temporalio.exceptions import ApplicationError
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Worker


class Message(BaseModel):
    content: datetime


@workflow.defn
class Workflow:
    @workflow.run
    async def run(self) -> None:
        message = Message(content=workflow.now())
        if not isinstance(message.content, datetime):
            raise ApplicationError(f"was type {type(message.content)}")


async def main():
    async with await WorkflowEnvironment.start_local() as env:
        task_queue = str(uuid4())
        async with Worker(env.client, workflows=[Workflow], task_queue=task_queue):
            await env.client.execute_workflow(
                Workflow.run, id=str(uuid4()), task_queue=task_queue
            )


if __name__ == "__main__":
    asyncio.run(main())

Debugging now…

0reactions
cretzcommented, Dec 2, 2022

For now, I have just marked this a known issue in the README in #219. If this becomes enough of an issue, we’ll have to consider patching datetime.__subclasscheck__.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Issues · temporalio/sdk-python - GitHub
[Bug] SandboxWorkflowRunner doesn't use correct Pydantic field types in some cases bug Something isn't working. #207 opened last month by michael-cybrid.
Read more >
Strict Type Validation With Pydantic - Section.io
This tutorial will guide the reader on how to type validate the inputs using Pydantic. We will also learn about create custom validators....
Read more >
Using different Pydantic models depending on the value of fields
How to make it so that in case of an error in filling in the fields, validator errors are returned only for a...
Read more >
Pydantic V2 Plan
If the input data has a SINGLE and INTUITIVE representation, in the field's type, AND no data is lost during the conversion, then...
Read more >
Cool Things You Can Do With Pydantic - Medium
Pydantic is a useful library for data parsing and validation. It coerces input types to the declared type (using type hints), ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found