"""Pydantic models for Python SAP Incentive Management Client.
These classes are generally not used directly but can be usefull
for type checking and type hints. Used to inherrit function on
all other models.
"""
from datetime import datetime
from importlib import import_module
from inspect import isclass
from types import ModuleType
from typing import Any, ClassVar, Literal, get_args, get_origin
from pydantic import (
AliasGenerator,
BaseModel,
ConfigDict,
Field,
field_validator,
)
from pydantic.alias_generators import to_camel
from pydantic.fields import FieldInfo
[docs]
class _BaseModel(BaseModel):
"""BaseModel inherited from ``pydantic.BaseModel``.
Contains the primary model_config which is required
for Pydantic to convert field names between
snake_case and camelCase when sending and recieving
json data from/to the SAP Incentive Management tenant.
"""
model_config: ClassVar[ConfigDict] = ConfigDict(
str_strip_whitespace=True,
str_min_length=1,
extra='allow',
populate_by_name=True,
use_enum_values=True,
validate_assignment=True,
alias_generator=AliasGenerator(alias=to_camel),
protected_namespaces=(
'model_computed_fields',
'model_config',
'model_construct',
'model_copy',
'model_dump',
'model_dump_json',
'model_extra',
'model_fields',
'model_fields_set',
'model_json_schema',
'model_parametrized_name',
'model_post_init',
'model_rebuild',
'model_validate',
'model_validate_json',
'model_validate_strings',
),
)
[docs]
@classmethod
def typed_fields(
cls,
typed: type | tuple[type, ...],
) -> dict[str, FieldInfo]:
"""Return model fields of the specified type.
This method can be usefull when converting data types for
example.
Returns:
A dictionary of attributes annotated with the specified type
where the keys are attribute names and the values are
`FieldInfo` objects.
"""
model_fields: dict[str, FieldInfo] = cls.model_fields
fields: dict[str, FieldInfo] = {}
def _process_type(
field_name: str,
field_info: FieldInfo,
field_type: type,
) -> None:
"""Recursively process types."""
if get_origin(field_type) is None:
if isclass(field_type) and issubclass(field_type, typed):
fields[field_name] = field_info
return
# If the field_type has an origin, process its generic arguments recursively
for arg in get_args(field_type):
_process_type(field_name, field_info, arg)
# Iterate through each field and process its type
for field_name, field_info in model_fields.items():
_process_type(
field_name=field_name,
field_info=field_info,
field_type=field_info.annotation, # type: ignore[arg-type]
)
return fields
class ErrorResponse(_BaseModel):
"""Base class for error responses.
Parameters:
time_stamp (datetime): Time of the response.
message (str): Description of the error.
"""
time_stamp: datetime
message: str
[docs]
class Endpoint(_BaseModel):
"""Base class for resources that can connect with the client.
Parameters:
attr_endpoint (str): URI endpoint to connect with
tenant. Must follow format ``api/v2/nameOfResource``.
Used by the client to construct the full request url.
"""
attr_endpoint: ClassVar[str]
[docs]
@classmethod
def expands(cls) -> dict[str, FieldInfo]:
"""Return model fields that refer to another model class.
This function is primarily used by the client to add the
``expand`` parameter to the request.
Returns:
A dictionary of attributes that can be expanded
where the keys are attribute names and the values are
``FieldInfo`` objects.
"""
return cls.typed_fields(Expandable)
[docs]
class Resource(Endpoint):
"""Base class for a resource.
Every resource has it's own attribute that uniquely
identifies the object on the tenant. Inheritance of
this class allows us to refer to the system unique
identifier without having to address it directly.
Example:
Attributes ``seq`` and ``credit_seq`` are
equal::
assert Credit.seq == Credit.credit_seq
Parameters:
attr_seq (str): Name of attribute that contains the
system unique identifier (seq).
"""
attr_seq: ClassVar[str]
@property
def seq(self) -> str | None:
"""System unique identifier (seq) of the resource instance."""
return getattr(self, self.attr_seq)
[docs]
class ValueUnitType(_BaseModel):
"""Unit Type of for ``Value``.
Parameters:
name (str): Name of the unit type.
unit_type_seq (str): System unique identifier.
"""
name: str
unit_type_seq: str
[docs]
class Value(_BaseModel):
"""Value object used by all numeric fields.
Parameters:
value (int | float | None): The amount.
unit_type (ValueUnitType): Type of amount.
"""
value: int | float | None
unit_type: ValueUnitType
[docs]
class ValueClass(_BaseModel):
"""Value Class, used only by ``UnitType``.
Parameters:
display_name (str): Name of the value class.
"""
display_name: str
[docs]
class AdjustmentContext(_BaseModel):
"""Adjustment Context for ``SalesTransaction``.
Used only when updating the value of a ``SalesTransaction``.
Parameters:
adjust_type_flag (Literal["adjustTo", "adjustBy", "reset"]):
- ``adjustTo``
- ``adjustBy``
- ``reset``
adjust_to_value (Value | None): Adjust value to this amount.
adjust_by_value (Value | None): Adjust value by this amount.
comment (str | None): Adjustment comment.
"""
adjust_type_flag: Literal['adjustTo', 'adjustBy', 'reset']
adjust_to_value: Value | None = None
adjust_by_value: Value | None = None
comment: str | None = None
class Generic16Mixin(_BaseModel):
"""Mixin to add generic fields to a model."""
ga1: str | None = Field(None, alias='genericAttribute1')
ga2: str | None = Field(None, alias='genericAttribute2')
ga3: str | None = Field(None, alias='genericAttribute3')
ga4: str | None = Field(None, alias='genericAttribute4')
ga5: str | None = Field(None, alias='genericAttribute5')
ga6: str | None = Field(None, alias='genericAttribute6')
ga7: str | None = Field(None, alias='genericAttribute7')
ga8: str | None = Field(None, alias='genericAttribute8')
ga9: str | None = Field(None, alias='genericAttribute9')
ga10: str | None = Field(None, alias='genericAttribute10')
ga11: str | None = Field(None, alias='genericAttribute11')
ga12: str | None = Field(None, alias='genericAttribute12')
ga13: str | None = Field(None, alias='genericAttribute13')
ga14: str | None = Field(None, alias='genericAttribute14')
ga15: str | None = Field(None, alias='genericAttribute15')
ga16: str | None = Field(None, alias='genericAttribute16')
gn1: Value | None = Field(None, alias='genericNumber1')
gn2: Value | None = Field(None, alias='genericNumber2')
gn3: Value | None = Field(None, alias='genericNumber3')
gn4: Value | None = Field(None, alias='genericNumber4')
gn5: Value | None = Field(None, alias='genericNumber5')
gn6: Value | None = Field(None, alias='genericNumber6')
gd1: datetime | None = Field(None, alias='genericDate1')
gd2: datetime | None = Field(None, alias='genericDate2')
gd3: datetime | None = Field(None, alias='genericDate3')
gd4: datetime | None = Field(None, alias='genericDate4')
gd5: datetime | None = Field(None, alias='genericDate5')
gd6: datetime | None = Field(None, alias='genericDate6')
gb1: bool | None = Field(None, alias='genericBoolean1')
gb2: bool | None = Field(None, alias='genericBoolean2')
gb3: bool | None = Field(None, alias='genericBoolean3')
gb4: bool | None = Field(None, alias='genericBoolean4')
gb5: bool | None = Field(None, alias='genericBoolean5')
gb6: bool | None = Field(None, alias='genericBoolean6')
class Generic32Mixin(Generic16Mixin):
"""Mixin to add generic fields to a model."""
ga17: str | None = Field(None, alias='genericAttribute17')
ga18: str | None = Field(None, alias='genericAttribute18')
ga19: str | None = Field(None, alias='genericAttribute19')
ga20: str | None = Field(None, alias='genericAttribute20')
ga21: str | None = Field(None, alias='genericAttribute21')
ga22: str | None = Field(None, alias='genericAttribute22')
ga23: str | None = Field(None, alias='genericAttribute23')
ga24: str | None = Field(None, alias='genericAttribute24')
ga25: str | None = Field(None, alias='genericAttribute25')
ga26: str | None = Field(None, alias='genericAttribute26')
ga27: str | None = Field(None, alias='genericAttribute27')
ga28: str | None = Field(None, alias='genericAttribute28')
ga29: str | None = Field(None, alias='genericAttribute29')
ga30: str | None = Field(None, alias='genericAttribute30')
ga31: str | None = Field(None, alias='genericAttribute31')
ga32: str | None = Field(None, alias='genericAttribute32')
class Expandable(_BaseModel):
"""Indicates expandable field.
Any model field that is annotated with a subclass of
``Expandable`` will be added to the ``expand`` parameter
when sending requests to the tenant.
"""
[docs]
class Reference(Expandable):
"""Expanded reference to a resource.
Parameters:
key (str): System unique identifier for the referred resource.
display_name (str): Name of the referred resource.
object_type (type[model.Resource]): Class of the referred resource.
key_string (str): Seems to always be the same as ``key``.
logical_keys (dict[str, str | int | Value | Any]): Some key
attributes of the referred resource.
"""
key: str
display_name: str
object_type: type[Resource]
key_string: str | None = None
logical_keys: dict[str, str | int | Value | Any]
@field_validator('object_type', mode='before')
@classmethod
def convert_object_type(cls, value: str) -> type[Resource]:
"""Convert string object_type to class."""
module: ModuleType = import_module('sapimclient.model')
if not (obj := getattr(module, value, None)):
msg = f'Could not find object type: {value}'
raise ValueError(msg)
if issubclass(obj, Resource) and obj is not Resource:
return obj
msg = f'Object type is not a subclass of Resource: {value}'
raise ValueError(msg)
def __str__(self) -> str:
"""Return key value."""
return self.key
[docs]
class SalesTransactionAssignment(Expandable, Generic16Mixin):
"""Expanded reference to a transaction assignment.
Parameters:
payee_id (str | None): Participant ID assigned to the transaction.
position_name (str | None): Position Name assigned to the transaction.
title_name (str | None): Title Name assigned to the transaction.
sales_order (str | None): Order ID of the transaction.
sales_transaction_seq (str): System unique identifier of the
transaction.
set_number (int | None): Index of the Assignment.
compensation_date (datetime | None): Compensation Date of the
transaction.
processing_unit (str | None): System unique identifier of the
Processing Unit.
ga{1-16} (str | None): Generic Attributes.
gn{1-6} (Value | None): Generic Numbers.
gd{1-6} (datetime | None): Generic Dates.
gb{1-6} (bool | None): Generic Booleans.
"""
payee_id: str | None = None
position_name: str | None = None
title_name: str | None = None
sales_order: str
sales_transaction_seq: str
set_number: int | None = None
compensation_date: datetime | None = None
processing_unit: str | None = None
[docs]
class Assignment(_BaseModel):
"""Assignment.
Used by ``Pipeline`` to refer to stage tables and by
``Plan``, ``Title`` and ``Position`` to refer to
Variable Assignments.
Parameters:
key (str | None): Not sure really.
owned_key (str | None): Also not sure really.
TODO: Is this an expandable reference?
"""
key: str | None = None
owned_key: str | None = None
[docs]
class BusinessUnitAssignment(_BaseModel):
"""Business Unit Assignment.
Used by ``AuditLog`` and ``Rule`` to refer to
Business Units.
Parameters:
mask (int): Not sure really.
smask (int): Seems to be the same as mask.
TODO: Is this an expandable reference?
"""
mask: int
smask: int
[docs]
class RuleUsage(_BaseModel):
"""Rule Usage.
Used by ``Rule`` and ``Rule Elements`` for some reason.
Parameters:
id (str): ID
name (str): Name
TODO: Is this an expandable reference?
"""
id: str
name: str
[docs]
class RuleUsageList(_BaseModel):
"""List of RuleUsage.
Parameters:
children (list[RuleUsage]): List of RuleUsage elements.
TODO: Is this an expandable reference?
TODO: Make this class accessible as iterator of ``RuleUsage``.
"""
children: list[RuleUsage]