Skip to content

Commit 7bb5ab0

Browse files
committed
Add date and datetime job parameter handling
1 parent 73271b5 commit 7bb5ab0

File tree

3 files changed

+344
-65
lines changed

3 files changed

+344
-65
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import datetime
2+
3+
from plain.models import Model, models_registry
4+
5+
6+
class 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+
31+
class 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+
56+
class 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+
79+
class 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+
100+
class 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
126+
PARAMETER_TYPES = [
127+
ModelParameter,
128+
DateTimeParameter,
129+
DateParameter,
130+
LegacyModelParameter,
131+
]
132+
133+
134+
class 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

plain-worker/plain/worker/registry.py

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,4 @@
1-
from plain.models import Model, models_registry
2-
3-
4-
class JobParameters:
5-
@staticmethod
6-
def to_json(args, kwargs):
7-
serialized_args = []
8-
for arg in args:
9-
if isinstance(arg, Model):
10-
serialized_args.append(ModelInstanceParameter.from_instance(arg))
11-
else:
12-
serialized_args.append(arg)
13-
14-
serialized_kwargs = {}
15-
for key, value in kwargs.items():
16-
if isinstance(value, Model):
17-
serialized_kwargs[key] = ModelInstanceParameter.from_instance(value)
18-
else:
19-
serialized_kwargs[key] = value
20-
21-
return {"args": serialized_args, "kwargs": serialized_kwargs}
22-
23-
@staticmethod
24-
def from_json(data):
25-
args = []
26-
for arg in data["args"]:
27-
if ModelInstanceParameter.is_gid(arg):
28-
args.append(ModelInstanceParameter.to_instance(arg))
29-
else:
30-
args.append(arg)
31-
32-
kwargs = {}
33-
for key, value in data["kwargs"].items():
34-
if ModelInstanceParameter.is_gid(value):
35-
kwargs[key] = ModelInstanceParameter.to_instance(value)
36-
else:
37-
kwargs[key] = value
38-
39-
return args, kwargs
40-
41-
42-
class ModelInstanceParameter:
43-
"""
44-
A string representation of a model instance,
45-
so we can convert a single parameter (model instance itself)
46-
into a string that can be serialized and stored in the database.
47-
"""
48-
49-
@staticmethod
50-
def from_instance(instance):
51-
return f"gid://{instance._meta.package_label}/{instance._meta.model_name}/{instance.id}"
52-
53-
@staticmethod
54-
def to_instance(s):
55-
if not s.startswith("gid://"):
56-
raise ValueError("Invalid ModelInstanceParameter string")
57-
package, model, obj_id = s[6:].split("/")
58-
model = models_registry.get_model(package, model)
59-
return model.objects.get(id=obj_id)
60-
61-
@staticmethod
62-
def is_gid(x):
63-
if not isinstance(x, str):
64-
return False
65-
return x.startswith("gid://")
1+
from .parameters import JobParameters
662

673

684
class JobsRegistry:

0 commit comments

Comments
 (0)