Skip to content

Commit d5b26b3

Browse files
committed
support datetime
1 parent e464954 commit d5b26b3

File tree

4 files changed

+161
-3
lines changed

4 files changed

+161
-3
lines changed

source/cbor.c

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ static PyObject *s_cbor_encoder_write_pyobject(struct aws_cbor_encoder *encoder,
200200

201201
PyObject *result = NULL;
202202

203-
/* Exact type matches first (no subclasses) */
203+
/* Exact type matches first (no subclasses) - fast path */
204204
if (type == (PyObject *)&PyLong_Type) {
205205
result = s_cbor_encoder_write_pylong(encoder, py_object);
206206
} else if (type == (PyObject *)&PyFloat_Type) {
@@ -219,8 +219,32 @@ static PyObject *s_cbor_encoder_write_pyobject(struct aws_cbor_encoder *encoder,
219219
/* Write py_dict, allow subclasses of `dict` */
220220
result = s_cbor_encoder_write_pydict(encoder, py_object);
221221
} else {
222-
/* Unsupported type */
223-
PyErr_Format(PyExc_ValueError, "Not supported type %R", type);
222+
/* Check for datetime using stable ABI (slower, so checked last) */
223+
int is_dt = aws_py_is_datetime_instance(py_object);
224+
if (is_dt < 0) {
225+
/* Error occurred during datetime check */
226+
result = NULL;
227+
} else if (is_dt > 0) {
228+
/* Convert datetime to CBOR epoch time (tag 1) */
229+
PyObject *timestamp_method = PyObject_GetAttrString(py_object, "timestamp");
230+
if (timestamp_method) {
231+
PyObject *timestamp = PyObject_CallNoArgs(timestamp_method);
232+
Py_DECREF(timestamp_method);
233+
if (timestamp) {
234+
/* Write CBOR tag 1 (epoch time) + timestamp */
235+
aws_cbor_encoder_write_tag(encoder, AWS_CBOR_TAG_EPOCH_TIME);
236+
result = s_cbor_encoder_write_pyobject_as_float(encoder, timestamp);
237+
Py_DECREF(timestamp);
238+
} else {
239+
result = NULL; /* timestamp() call failed */
240+
}
241+
} else {
242+
result = NULL; /* Failed to get timestamp method */
243+
}
244+
} else {
245+
/* Unsupported type */
246+
PyErr_Format(PyExc_ValueError, "Not supported type %R", type);
247+
}
224248
}
225249

226250
/* Release the type reference */

source/module.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,45 @@
3636
static struct aws_logger s_logger;
3737
static bool s_logger_init = false;
3838

39+
/*******************************************************************************
40+
* DateTime Type Cache (for stable ABI compatibility)
41+
******************************************************************************/
42+
static PyObject *s_datetime_class = NULL;
43+
44+
static int s_init_datetime_cache(void) {
45+
if (s_datetime_class) {
46+
return 0; /* Already initialized */
47+
}
48+
49+
/* Import datetime module */
50+
PyObject *datetime_module = PyImport_ImportModule("datetime");
51+
if (!datetime_module) {
52+
return -1;
53+
}
54+
55+
/* Get datetime class - new reference we'll keep */
56+
s_datetime_class = PyObject_GetAttrString(datetime_module, "datetime");
57+
Py_DECREF(datetime_module);
58+
59+
if (!s_datetime_class) {
60+
return -1;
61+
}
62+
63+
return 0;
64+
}
65+
66+
static void s_cleanup_datetime_cache(void) {
67+
Py_XDECREF(s_datetime_class);
68+
s_datetime_class = NULL;
69+
}
70+
71+
int aws_py_is_datetime_instance(PyObject *obj) {
72+
if (!s_datetime_class && s_init_datetime_cache() < 0) {
73+
return -1;
74+
}
75+
return PyObject_IsInstance(obj, s_datetime_class);
76+
}
77+
3978
PyObject *aws_py_init_logging(PyObject *self, PyObject *args) {
4079
(void)self;
4180

@@ -1035,6 +1074,12 @@ PyMODINIT_FUNC PyInit__awscrt(void) {
10351074
aws_register_error_info(&s_error_list);
10361075
s_error_map_init();
10371076

1077+
/* Initialize datetime type cache for stable ABI datetime support */
1078+
if (s_init_datetime_cache() < 0) {
1079+
/* Non-fatal: datetime encoding will fail but rest of module works */
1080+
PyErr_Clear();
1081+
}
1082+
10381083
return m;
10391084
}
10401085

source/module.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ struct aws_byte_cursor aws_byte_cursor_from_pyunicode(PyObject *str);
7070
* If conversion cannot occur, cursor->ptr will be NULL and a python exception is set */
7171
struct aws_byte_cursor aws_byte_cursor_from_pybytes(PyObject *py_bytes);
7272

73+
/**
74+
* Check if a PyObject is an instance of datetime.datetime using stable ABI.
75+
* Returns:
76+
* 1 if obj is a datetime instance
77+
* 0 if obj is not a datetime instance
78+
* -1 on error (Python exception set)
79+
*/
80+
int aws_py_is_datetime_instance(PyObject *obj);
81+
7382
/* Set current thread's error indicator based on aws_last_error() */
7483
void PyErr_SetAwsLastError(void);
7584

test/test_cbor.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,86 @@ def on_epoch_time(epoch_secs):
151151
exception = e
152152
self.assertIsNotNone(exception)
153153

154+
def test_cbor_encode_decode_datetime(self):
155+
"""Test automatic datetime encoding/decoding"""
156+
# Create a datetime object
157+
dt = datetime.datetime(2024, 1, 1, 12, 0, 0)
158+
159+
# Encode datetime - should automatically convert to CBOR tag 1 + timestamp
160+
encoder = AwsCborEncoder()
161+
encoder.write_data_item(dt)
162+
163+
# Decode with callback to convert back to datetime
164+
def on_epoch_time(epoch_secs):
165+
return datetime.datetime.fromtimestamp(epoch_secs)
166+
167+
decoder = AwsCborDecoder(encoder.get_encoded_data(), on_epoch_time)
168+
result = decoder.pop_next_data_item()
169+
170+
# Verify the result matches original datetime
171+
self.assertEqual(dt, result)
172+
self.assertIsInstance(result, datetime.datetime)
173+
174+
# Test datetime with microsecond precision (milliseconds)
175+
dt_with_microseconds = datetime.datetime(2024, 1, 1, 12, 0, 0, 123456) # 123.456 milliseconds
176+
encoder3 = AwsCborEncoder()
177+
encoder3.write_data_item(dt_with_microseconds)
178+
179+
decoder3 = AwsCborDecoder(encoder3.get_encoded_data(), on_epoch_time)
180+
result_microseconds = decoder3.pop_next_data_item()
181+
182+
# Verify microsecond precision is preserved
183+
self.assertEqual(dt_with_microseconds, result_microseconds)
184+
self.assertEqual(dt_with_microseconds.microsecond, result_microseconds.microsecond)
185+
self.assertIsInstance(result_microseconds, datetime.datetime)
186+
187+
# Test datetime in a list
188+
encoder2 = AwsCborEncoder()
189+
test_list = [dt, "text", 123, dt_with_microseconds]
190+
encoder2.write_data_item(test_list)
191+
192+
decoder2 = AwsCborDecoder(encoder2.get_encoded_data(), on_epoch_time)
193+
result_list = decoder2.pop_next_data_item()
194+
195+
self.assertEqual(len(result_list), 4)
196+
self.assertEqual(result_list[0], dt)
197+
self.assertEqual(result_list[1], "text")
198+
self.assertEqual(result_list[2], 123)
199+
self.assertEqual(result_list[3], dt_with_microseconds)
200+
# Verify microsecond precision in list
201+
self.assertEqual(result_list[3].microsecond, 123456)
202+
203+
def test_cbor_encode_unsupported_type(self):
204+
"""Test that encoding unsupported types raises ValueError"""
205+
# Create a custom class that's not supported by CBOR encoder
206+
class CustomClass:
207+
def __init__(self, value):
208+
self.value = value
209+
210+
# Try to encode an unsupported type
211+
encoder = AwsCborEncoder()
212+
unsupported_obj = CustomClass(42)
213+
214+
# Should raise ValueError with message about unsupported type
215+
with self.assertRaises(ValueError) as context:
216+
encoder.write_data_item(unsupported_obj)
217+
# Verify the error message mentions "Not supported type"
218+
self.assertIn("Not supported type", str(context.exception))
219+
220+
# Test unsupported type in a list (should also fail)
221+
encoder2 = AwsCborEncoder()
222+
with self.assertRaises(ValueError) as context2:
223+
encoder2.write_data_item([1, 2, unsupported_obj, 3])
224+
225+
self.assertIn("Not supported type", str(context2.exception))
226+
227+
# Test unsupported type as dict key (should also fail)
228+
encoder3 = AwsCborEncoder()
229+
with self.assertRaises(ValueError) as context3:
230+
encoder3.write_data_item({unsupported_obj: "value"})
231+
232+
self.assertIn("Not supported type", str(context3.exception))
233+
154234
def _ieee754_bits_to_float(self, bits):
155235
return struct.unpack('>f', struct.pack('>I', bits))[0]
156236

0 commit comments

Comments
 (0)