Plain is headed towards 1.0! Subscribe for development updates →

  1import datetime
  2
  3from plain.models import Model, models_registry
  4
  5
  6class JobParameter:
  7    """Base class for job parameter serialization/deserialization."""
  8
  9    STR_PREFIX = None  # Subclasses should define this
 10
 11    @classmethod
 12    def serialize(cls, value):
 13        """Return serialized string or None if can't handle this value."""
 14        return None
 15
 16    @classmethod
 17    def deserialize(cls, data):
 18        """Return deserialized value or None if can't handle this data."""
 19        return None
 20
 21    @classmethod
 22    def _extract_string_value(cls, data):
 23        """Extract value from string with prefix, return None if invalid format."""
 24        if not isinstance(data, str) or not cls.STR_PREFIX:
 25            return None
 26        if not data.startswith(cls.STR_PREFIX) or len(data) <= len(cls.STR_PREFIX):
 27            return None
 28        return data[len(cls.STR_PREFIX) :]
 29
 30
 31class ModelParameter(JobParameter):
 32    """Handle Plain model instances using a new string format."""
 33
 34    STR_PREFIX = "__plain://model/"
 35
 36    @classmethod
 37    def serialize(cls, value):
 38        if isinstance(value, Model):
 39            return f"{cls.STR_PREFIX}{value._meta.package_label}/{value._meta.model_name}/{value.id}"
 40        return None
 41
 42    @classmethod
 43    def deserialize(cls, data):
 44        if value_part := cls._extract_string_value(data):
 45            try:
 46                parts = value_part.split("/")
 47                if len(parts) == 3 and all(parts):
 48                    package, model_name, obj_id = parts
 49                    model = models_registry.get_model(package, model_name)
 50                    return model.objects.get(id=obj_id)
 51            except (ValueError, Exception):
 52                pass
 53        return None
 54
 55
 56class DateParameter(JobParameter):
 57    """Handle date objects."""
 58
 59    STR_PREFIX = "__plain://date/"
 60
 61    @classmethod
 62    def serialize(cls, value):
 63        if isinstance(value, datetime.date) and not isinstance(
 64            value, datetime.datetime
 65        ):
 66            return f"{cls.STR_PREFIX}{value.isoformat()}"
 67        return None
 68
 69    @classmethod
 70    def deserialize(cls, data):
 71        if value_part := cls._extract_string_value(data):
 72            try:
 73                return datetime.date.fromisoformat(value_part)
 74            except ValueError:
 75                pass
 76        return None
 77
 78
 79class DateTimeParameter(JobParameter):
 80    """Handle datetime objects."""
 81
 82    STR_PREFIX = "__plain://datetime/"
 83
 84    @classmethod
 85    def serialize(cls, value):
 86        if isinstance(value, datetime.datetime):
 87            return f"{cls.STR_PREFIX}{value.isoformat()}"
 88        return None
 89
 90    @classmethod
 91    def deserialize(cls, data):
 92        if value_part := cls._extract_string_value(data):
 93            try:
 94                return datetime.datetime.fromisoformat(value_part)
 95            except ValueError:
 96                pass
 97        return None
 98
 99
100class LegacyModelParameter(JobParameter):
101    """Legacy model parameter handling for backwards compatibility."""
102
103    STR_PREFIX = "gid://"
104
105    @classmethod
106    def serialize(cls, value):
107        # Don't serialize new instances with legacy format
108        return None
109
110    @classmethod
111    def deserialize(cls, data):
112        if value_part := cls._extract_string_value(data):
113            try:
114                package, model, obj_id = value_part.split("/")
115                model = models_registry.get_model(package, model)
116                return model.objects.get(id=obj_id)
117            except (ValueError, Exception):
118                pass
119        return None
120
121
122# Registry of parameter types to check in order
123# The order matters - more specific types should come first
124# DateTimeParameter must come before DateParameter since datetime is a subclass of date
125# LegacyModelParameter is last since it only handles deserialization
126PARAMETER_TYPES = [
127    ModelParameter,
128    DateTimeParameter,
129    DateParameter,
130    LegacyModelParameter,
131]
132
133
134class JobParameters:
135    """
136    Main interface for serializing and deserializing job parameters.
137    Uses the registered parameter types to handle different value types.
138    """
139
140    @staticmethod
141    def to_json(args, kwargs):
142        serialized_args = []
143        for arg in args:
144            serialized = JobParameters._serialize_value(arg)
145            serialized_args.append(serialized)
146
147        serialized_kwargs = {}
148        for key, value in kwargs.items():
149            serialized = JobParameters._serialize_value(value)
150            serialized_kwargs[key] = serialized
151
152        return {"args": serialized_args, "kwargs": serialized_kwargs}
153
154    @staticmethod
155    def _serialize_value(value):
156        """Serialize a single value using the registered parameter types."""
157        # Try each parameter type to see if it can serialize this value
158        for param_type in PARAMETER_TYPES:
159            result = param_type.serialize(value)
160            if result is not None:
161                return result
162
163        # If no parameter type can handle it, return as-is
164        return value
165
166    @staticmethod
167    def from_json(data):
168        args = []
169        for arg in data["args"]:
170            deserialized = JobParameters._deserialize_value(arg)
171            args.append(deserialized)
172
173        kwargs = {}
174        for key, value in data["kwargs"].items():
175            deserialized = JobParameters._deserialize_value(value)
176            kwargs[key] = deserialized
177
178        return args, kwargs
179
180    @staticmethod
181    def _deserialize_value(value):
182        """Deserialize a single value using the registered parameter types."""
183        # Try each parameter type to see if it can deserialize this value
184        for param_type in PARAMETER_TYPES:
185            result = param_type.deserialize(value)
186            if result is not None:
187                return result
188
189        # If no parameter type can handle it, return as-is
190        return value