dyntastic icon indicating copy to clipboard operation
dyntastic copied to clipboard

Handle 'floats' when Dict is of type 'any'

Open alexiswl opened this issue 6 months ago • 1 comments

I have a model of the following type for a workflow engine

Setup permalink - https://github.com/OrcaBus/service-icav2-wes-manager/blob/main/app/interface/icav2_wes_api/models/analysis.py#L29-L141

class Icav2WesAnalysisBase(BaseModel):
    name: str
    inputs: Dict[str, Any]
    ...
    tags: Dict[str, Any]

class Icav2WesAnalysisOrcabusId(BaseModel):
    # fqr.ABCDEFGHIJKLMNOP
    # BCLConvert Metadata attributes
    id: str = Field(default_factory=lambda: f"{ICAV2_WES_ANALYSIS_PREFIX}.{get_ulid()}")

class Icav2WesAnalysisWithId(Icav2WesAnalysisBase, Icav2WesAnalysisOrcabusId):
    """
    Order class inheritance this way to ensure that the id field is set first
    """
    # We also have the steps execution id as an attribute to add
    status: AnalysisStatus = Field(default='PENDING')
    submission_time: datetime = Field(default_factory=datetime.now)
    steps_launch_execution_arn: Optional[str] = None
    icav2_analysis_id: Optional[str] = None
    start_time: Optional[datetime] = None
    end_time: Optional[datetime] = None


class Icav2WesAnalysisData(Icav2WesAnalysisWithId, Dyntastic):
    """
    The job data object
    """
    __table_name__ = environ['DYNAMODB_ICAV2_WES_ANALYSIS_TABLE_NAME']
    __table_host__ = environ['DYNAMODB_HOST']
    __hash_key__ = "id"

    # To Dictionary
    def to_dict(self) -> 'Icav2WesAnalysisResponse':
        """
        Alternative serialization path to return objects by camel case
        :return:
        """
        return jsonable_encoder(
            Icav2WesAnalysisResponse(
                **self.model_dump()
            ).model_dump(by_alias=True)
        )

However I get the following error if any of the input values have a float in them, i.e

Icav2WesAnalysisData(
    id='iwa.01JXVPNHYZ8Z3W7MM66J8C3Z46',
    name='umccr--automated--dragen-wgts-dna--4-4-4--20250616efgh6789',
    inputs=...,
    engine_parameters=...,
    tags={
      ...
      'preLaunchCoverageEst': 44.41, 
      'preLaunchDupFracEst': 0.3, 
      ...
      },
      status='PENDING',
      submission_time=datetime.datetime(2025, 6, 16, 6, 26, 16, 159186),
      steps_launch_execution_arn=None,
      icav2_analysis_id=None,
      start_time=None,
      end_time=None
    }
).save()
Click to expand!
Traceback (most recent call last):
  File "/var/task/mangum/protocols/http.py", line 58, in run
    await app(self.scope, self.receive, self.send)
  File "/var/task/fastapi/applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "/var/task/starlette/applications.py", line 112, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/var/task/starlette/middleware/errors.py", line 187, in __call__
    raise exc
  File "/var/task/starlette/middleware/errors.py", line 165, in __call__
    await self.app(scope, receive, _send)
  File "/var/task/starlette/middleware/exceptions.py", line 62, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/var/task/starlette/_exception_handler.py", line 53, in wrapped_app
    raise exc
  File "/var/task/starlette/_exception_handler.py", line 42, in wrapped_app
    await app(scope, receive, sender)
  File "/var/task/starlette/routing.py", line 714, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/var/task/starlette/routing.py", line 734, in app
    await route.handle(scope, receive, send)
  File "/var/task/starlette/routing.py", line 288, in handle
    await self.app(scope, receive, send)
  File "/var/task/starlette/routing.py", line 76, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "/var/task/starlette/_exception_handler.py", line 53, in wrapped_app
    raise exc
  File "/var/task/starlette/_exception_handler.py", line 42, in wrapped_app
    await app(scope, receive, sender)
  File "/var/task/starlette/routing.py", line 73, in app
    response = await f(request)
               ^^^^^^^^^^^^^^^^
  File "/var/task/fastapi/routing.py", line 301, in app
    raw_response = await run_endpoint_function(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/fastapi/routing.py", line 212, in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/icav2_wes_api/api/analysis.py", line 176, in create_job
    analysis_obj.save()
  File "/var/task/dyntastic/main.py", line 342, in save
    return self._dyntastic_call("put_item", Item=dynamo_serialized, ConditionExpression=condition)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/dyntastic/main.py", line 676, in _dyntastic_call
    return method(**filtered_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/boto3/resources/factory.py", line 581, in do_action
    response = action(self, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/boto3/resources/action.py", line 88, in __call__
    response = getattr(parent.meta.client, operation_name)(*args, **params)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/botocore/client.py", line 598, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/botocore/context.py", line 123, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/botocore/client.py", line 1002, in _make_api_call
    api_params = self._emit_api_params(
                 ^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/botocore/client.py", line 1121, in _emit_api_params
    self.meta.events.emit(
  File "/var/task/botocore/hooks.py", line 412, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/botocore/hooks.py", line 256, in emit
    return self._emit(event_name, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/botocore/hooks.py", line 239, in _emit
    response = handler(**kwargs)
               ^^^^^^^^^^^^^^^^^
  File "/var/task/boto3/dynamodb/transform.py", line 217, in inject_attribute_value_input
    self._transformer.transform(
  File "/var/task/boto3/dynamodb/transform.py", line 289, in transform
    self._transform_parameters(model, params, transformation, target_shape)
  File "/var/task/boto3/dynamodb/transform.py", line 296, in _transform_parameters
    getattr(self, f'_transform_{type_name}')(
  File "/var/task/boto3/dynamodb/transform.py", line 312, in _transform_structure
    self._transform_parameters(
  File "/var/task/boto3/dynamodb/transform.py", line 296, in _transform_parameters
    getattr(self, f'_transform_{type_name}')(
  File "/var/task/boto3/dynamodb/transform.py", line 326, in _transform_map
    params[key] = transformation(value)
                  ^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/boto3/dynamodb/types.py", line 116, in serialize
    return {dynamodb_type: serializer(value)}
                           ^^^^^^^^^^^^^^^^^
  File "/var/task/boto3/dynamodb/types.py", line 240, in _serialize_m
    return {k: self.serialize(v) for k, v in value.items()}
               ^^^^^^^^^^^^^^^^^
  File "/var/task/boto3/dynamodb/types.py", line 114, in serialize
    dynamodb_type = self._get_dynamodb_type(value)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/boto3/dynamodb/types.py", line 127, in _get_dynamodb_type
    elif self._is_number(value):
         ^^^^^^^^^^^^^^^^^^^^^^
  File "/var/task/boto3/dynamodb/types.py", line 171, in _is_number
    raise TypeError(
TypeError: Float types are not supported. Use Decimal types instead.

alexiswl avatar Jun 16 '25 07:06 alexiswl

Since it's not possible to 'schemafy' the inputs or tags for my use-case, as a workaround I json.dumps and then json.loads when generating an API response

alexiswl avatar Jun 16 '25 07:06 alexiswl