python-datamodel 0.10.1__cp313-cp313-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. datamodel/__init__.py +13 -0
  2. datamodel/abstract.py +383 -0
  3. datamodel/adaptive/__init__.py +0 -0
  4. datamodel/adaptive/models.py +598 -0
  5. datamodel/aliases/__init__.py +26 -0
  6. datamodel/base.py +180 -0
  7. datamodel/converters.c +43471 -0
  8. datamodel/converters.cp313-win32.pyd +0 -0
  9. datamodel/converters.html +17387 -0
  10. datamodel/converters.pyx +1489 -0
  11. datamodel/exceptions.c +13455 -0
  12. datamodel/exceptions.cp313-win32.pyd +0 -0
  13. datamodel/exceptions.html +1261 -0
  14. datamodel/exceptions.pxd +13 -0
  15. datamodel/exceptions.pyx +50 -0
  16. datamodel/fields.cp313-win32.pyd +0 -0
  17. datamodel/fields.cpp +17401 -0
  18. datamodel/fields.html +3912 -0
  19. datamodel/fields.pyx +309 -0
  20. datamodel/functions.cp313-win32.pyd +0 -0
  21. datamodel/functions.cpp +9068 -0
  22. datamodel/functions.html +1766 -0
  23. datamodel/functions.pxd +9 -0
  24. datamodel/functions.pyx +82 -0
  25. datamodel/jsonld/__init__.py +45 -0
  26. datamodel/jsonld/models.py +500 -0
  27. datamodel/libs/__init__.py +1 -0
  28. datamodel/libs/mapping.c +15067 -0
  29. datamodel/libs/mapping.cp313-win32.pyd +0 -0
  30. datamodel/libs/mapping.html +2618 -0
  31. datamodel/libs/mapping.pxd +11 -0
  32. datamodel/libs/mapping.pyx +135 -0
  33. datamodel/libs/mutables.py +127 -0
  34. datamodel/models.py +814 -0
  35. datamodel/parsers/__init__.py +0 -0
  36. datamodel/parsers/encoders.py +15 -0
  37. datamodel/parsers/json.cp313-win32.pyd +0 -0
  38. datamodel/parsers/json.cpp +17004 -0
  39. datamodel/parsers/json.html +3365 -0
  40. datamodel/parsers/json.pyx +250 -0
  41. datamodel/profiler.py +21 -0
  42. datamodel/py.typed +0 -0
  43. datamodel/rs_core/Cargo.toml +17 -0
  44. datamodel/rs_core/src/lib.rs +294 -0
  45. datamodel/rs_parsers/Cargo.toml +22 -0
  46. datamodel/rs_parsers/src/lib.rs +571 -0
  47. datamodel/rs_parsers.cp313-win32.pyd +0 -0
  48. datamodel/rs_validators/Cargo.toml +17 -0
  49. datamodel/rs_validators/src/lib.rs +0 -0
  50. datamodel/typedefs/__init__.py +9 -0
  51. datamodel/typedefs/singleton.c +9169 -0
  52. datamodel/typedefs/singleton.cp313-win32.pyd +0 -0
  53. datamodel/typedefs/singleton.html +629 -0
  54. datamodel/typedefs/singleton.pxd +9 -0
  55. datamodel/typedefs/singleton.pyx +24 -0
  56. datamodel/typedefs/types.c +11716 -0
  57. datamodel/typedefs/types.cp313-win32.pyd +0 -0
  58. datamodel/typedefs/types.html +732 -0
  59. datamodel/typedefs/types.pxd +11 -0
  60. datamodel/typedefs/types.pyx +39 -0
  61. datamodel/types.c +7165 -0
  62. datamodel/types.cp313-win32.pyd +0 -0
  63. datamodel/types.html +716 -0
  64. datamodel/types.pyx +100 -0
  65. datamodel/validation.cp313-win32.pyd +0 -0
  66. datamodel/validation.cpp +17085 -0
  67. datamodel/validation.html +4769 -0
  68. datamodel/validation.pyx +315 -0
  69. datamodel/version.py +13 -0
  70. examples/nn/examples.py +311 -0
  71. examples/nn/stores.py +151 -0
  72. examples/tests/sp_types.py +294 -0
  73. examples/tests/speed_dates.py +26 -0
  74. python_datamodel-0.10.1.dist-info/LICENSE +29 -0
  75. python_datamodel-0.10.1.dist-info/METADATA +320 -0
  76. python_datamodel-0.10.1.dist-info/RECORD +78 -0
  77. python_datamodel-0.10.1.dist-info/WHEEL +5 -0
  78. python_datamodel-0.10.1.dist-info/top_level.txt +7 -0
@@ -0,0 +1,571 @@
1
+ use pyo3::prelude::*;
2
+ use pyo3::exceptions::PyValueError;
3
+ use pyo3::PyTypeInfo;
4
+ use pyo3::wrap_pyfunction;
5
+ use pyo3::types::{PyDate, PyDateTime, PyAny, PyString, PyBool, PyBytes, PyInt, PyFloat};
6
+ use chrono::{Datelike, Timelike, NaiveDate, NaiveDateTime, DateTime, Utc};
7
+ use speedate::Date as SpeeDate;
8
+ use speedate::DateTime as SpeeDateTime;
9
+ use uuid::Uuid;
10
+ use rust_decimal::Decimal; // Rust Decimal crate
11
+ use rust_decimal::prelude::FromStr;
12
+ // use speedate::{Date, DateTime, ParseError};
13
+ // use std::collections::HashMap;
14
+ // NaiveTime
15
+
16
+
17
+ #[pyfunction]
18
+ #[pyo3(signature = (obj=None))]
19
+ fn to_string(py: Python, obj: Option<Py<PyAny>>) -> PyResult<Option<String>> {
20
+ // If the object is None, return None
21
+ match obj {
22
+ None => Ok(None),
23
+ Some(py_obj) => {
24
+ let val = py_obj.bind(py);
25
+ if val.is_none() {
26
+ Ok(None)
27
+ } else if val.is_instance(&PyString::type_object(py))? {
28
+ // If the object is already a string, return it
29
+ Ok(Some(val.extract::<String>()?))
30
+ } else if val.is_instance(&PyBytes::type_object(py))? {
31
+ // If the object is bytes, decode it to a string
32
+ let bytes = val.downcast::<PyBytes>()?;
33
+ Ok(Some(String::from_utf8(bytes.as_bytes().to_vec())?))
34
+ } else if val.is_callable() {
35
+ // If the object is callable, call it and convert the result to a string
36
+ match val.call0()?.extract::<String>() {
37
+ Ok(result) => Ok(Some(result)),
38
+ Err(_) => Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
39
+ "Callable did not return a string",
40
+ )),
41
+ }
42
+ } else {
43
+ // Try converting the object to a string
44
+ match val.str()?.to_str() {
45
+ Ok(string_rep) => Ok(Some(string_rep.to_string())),
46
+ Err(_) => Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
47
+ "Object could not be converted to a string",
48
+ )),
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+
56
+ #[pyfunction]
57
+ #[pyo3(signature = (obj))]
58
+ fn slugify_camelcase(obj: String) -> String {
59
+ // Return the original string if it's empty
60
+ if obj.is_empty() {
61
+ return obj;
62
+ }
63
+
64
+ // Initialize the resulting string with the first character
65
+ let mut slugified = String::with_capacity(obj.len());
66
+ let mut chars = obj.chars();
67
+ if let Some(first_char) = chars.next() {
68
+ slugified.push(first_char);
69
+ }
70
+
71
+ // Process the rest of the string
72
+ for c in chars {
73
+ // Check if the current character is uppercase and the previous character isn't a space
74
+ if c.is_uppercase() && !slugified.ends_with(' ') {
75
+ slugified.push(' '); // Insert a space before the uppercase character
76
+ }
77
+ slugified.push(c);
78
+ }
79
+
80
+ slugified
81
+ }
82
+
83
+ /// Converts a string representation of truth to a boolean.
84
+ ///
85
+ /// # Arguments
86
+ /// * `val` - The string value to convert.
87
+ ///
88
+ /// # Returns
89
+ /// * `Ok(bool)` - True or False if conversion is successful.
90
+ /// * `Err(PyValueError)` - Raised if the input string cannot be interpreted as boolean.
91
+ #[pyfunction]
92
+ #[pyo3(signature = (val))]
93
+ fn strtobool(val: &str) -> PyResult<bool> {
94
+ match val.trim().to_lowercase().as_str() {
95
+ "y" | "yes" | "t" | "true" | "on" | "1" => Ok(true),
96
+ "n" | "no" | "f" | "false" | "off" | "0" | "none" | "null" => Ok(false),
97
+ _ => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
98
+ "Invalid boolean string: '{}'",
99
+ val
100
+ ))),
101
+ }
102
+ }
103
+
104
+ /// Converts any object value to a boolean representation.
105
+ ///
106
+ /// # Arguments
107
+ /// * `obj` - A PyObject to convert.
108
+ ///
109
+ /// # Returns
110
+ /// * Boolean or None if the object is None.
111
+ #[pyfunction]
112
+ #[pyo3(signature = (obj=None))]
113
+ fn to_boolean(py: Python, obj: Option<Py<PyAny>>) -> PyResult<Option<bool>> {
114
+ match obj {
115
+ Some(val) => {
116
+ let val_ref = val.bind(py);
117
+ if val_ref.is_none() {
118
+ Ok(None)
119
+ } else if val_ref.is_instance(&PyBool::type_object(py))? {
120
+ Ok(Some(val.extract::<bool>(py)?))
121
+ } else if val_ref.is_instance(&PyString::type_object(py))? {
122
+ let py_str = val_ref.downcast::<PyString>()?;
123
+ Ok(Some(strtobool(py_str.to_str()?)?))
124
+ } else if let Ok(b) = val_ref.call_method0("__bool__")?.extract::<bool>() {
125
+ Ok(Some(b))
126
+ } else {
127
+ Ok(Some(false))
128
+ }
129
+ }
130
+ None => Ok(None),
131
+ }
132
+ }
133
+
134
+
135
+ // Return the timestamp (int, float) as a Python datetime object.
136
+ #[pyfunction]
137
+ #[pyo3(signature = (timestamp))]
138
+ fn to_timestamp(py: Python, timestamp: f64) -> PyResult<Py<PyDateTime>> {
139
+ // Split the timestamp into seconds and microseconds
140
+ let seconds = timestamp.floor() as i64;
141
+ let microseconds = ((timestamp - seconds as f64) * 1_000_000.0).round() as u32;
142
+
143
+ // Validate the timestamp range to prevent panic
144
+ if seconds < NaiveDateTime::MIN.and_utc().timestamp() || seconds > NaiveDateTime::MAX.and_utc().timestamp() {
145
+ return Err(PyValueError::new_err("Invalid timestamp"));
146
+ }
147
+
148
+ // Create a NaiveDateTime from the split values
149
+ if let Some(datetime) = DateTime::from_timestamp(seconds, microseconds) {
150
+ // Construct a PyDateTime object
151
+ let py_date = PyDateTime::new(
152
+ py,
153
+ datetime.date_naive().year(),
154
+ datetime.date_naive().month() as u8,
155
+ datetime.date_naive().day() as u8,
156
+ datetime.time().hour() as u8,
157
+ datetime.time().minute() as u8,
158
+ datetime.time().second() as u8,
159
+ datetime.time().nanosecond() / 1_000, // Convert nanoseconds to microseconds
160
+ None,
161
+ )?;
162
+ Ok(py_date.into())
163
+ } else {
164
+ // Handle invalid timestamps
165
+ Err(PyValueError::new_err("Invalid timestamp"))
166
+ }
167
+ }
168
+
169
+
170
+ /// Parses a string into a `NaiveDate` using multiple formats.
171
+ ///
172
+ /// # Arguments
173
+ /// * `input` - The string to parse.
174
+ ///
175
+ /// # Returns
176
+ /// * `Ok(NaiveDate)` if parsing succeeds.
177
+ /// * `Err(PyValueError)` if no format matches.
178
+ #[pyfunction]
179
+ #[pyo3(signature = (input, custom_format=None))]
180
+ fn to_date(py: Python, input: &str, custom_format: Option<&str>) -> PyResult<Py<PyDate>> {
181
+ // Raise an error if the input is empty
182
+ if input.trim().is_empty() {
183
+ return Err(PyValueError::new_err("Input string is empty"));
184
+ }
185
+
186
+ // Attempt parsing using Speedate
187
+ if let Ok(parsed_date) = SpeeDate::parse_str(input) {
188
+ return Ok(PyDate::new(
189
+ py,
190
+ parsed_date.year as i32,
191
+ parsed_date.month as u8,
192
+ parsed_date.day as u8,
193
+ )?.into());
194
+ }
195
+
196
+ // Define custom formats to try, including the optional format.
197
+ let mut formats = vec![
198
+ "%Y-%m-%d", // ISO 8601 date
199
+ "%m/%d/%Y", // Month/day/year
200
+ "%m-%d-%Y", // Month-day-year
201
+ "%d-%m-%Y", // Custom format
202
+ "%Y/%m/%d", // Slash-separated date
203
+ "%Y-%m-%dT%H:%M:%S%.f", // ISO 8601 datetime
204
+ "%Y-%m-%d %H:%M:%S", // ISO 8601 with time
205
+ "%d/%m/%Y", // Day/month/year
206
+ "%d.%m.%Y", // Day.month.year
207
+ ];
208
+
209
+ if let Some(fmt) = custom_format {
210
+ formats.push(fmt);
211
+ }
212
+
213
+ // Attempt parsing with each format
214
+ for &fmt in &formats {
215
+ if let Ok(date) = NaiveDate::parse_from_str(input, fmt) {
216
+ return Ok(PyDate::new(
217
+ py,
218
+ date.year(),
219
+ date.month() as u8,
220
+ date.day() as u8,
221
+ )?.into());
222
+ }
223
+ }
224
+
225
+ // Raise an error if no format matched
226
+ Err(PyValueError::new_err(format!(
227
+ "Unable to parse input '{}' into a date. Accepted formats: {:?}",
228
+ input, formats
229
+ )))
230
+ }
231
+
232
+
233
+ // Convert a string representation to a datetime object.
234
+ #[pyfunction]
235
+ #[pyo3(signature = (input, custom_format=None))]
236
+ fn to_datetime(py: Python, input: &str, custom_format: Option<&str>) -> PyResult<Py<PyDateTime>> {
237
+
238
+ if input.trim().is_empty() {
239
+ return Err(PyValueError::new_err("Input string is empty"));
240
+ }
241
+
242
+ // Attempt parsing using Speedate
243
+ // Attempt parsing using Speedate
244
+ if let Ok(parsed_datetime) = SpeeDateTime::parse_str(input) {
245
+ return Ok(PyDateTime::new(
246
+ py,
247
+ parsed_datetime.date.year as i32,
248
+ parsed_datetime.date.month,
249
+ parsed_datetime.date.day,
250
+ parsed_datetime.time.hour,
251
+ parsed_datetime.time.minute,
252
+ parsed_datetime.time.second,
253
+ parsed_datetime.time.microsecond,
254
+ None,
255
+ )?.into());
256
+ }
257
+
258
+ // Try parsing as ISO 8601 datetime with timezone.
259
+ if let Ok(datetime) = DateTime::parse_from_rfc3339(input) {
260
+ let datetime_utc = datetime.with_timezone(&Utc);
261
+ return Ok(
262
+ PyDateTime::from_timestamp(py, datetime_utc.timestamp() as f64, None,)?.into()
263
+ );
264
+ }
265
+
266
+ // Define custom formats to try, including the optional format.
267
+ let mut formats = vec![
268
+ "%Y-%m-%d", // ISO 8601 date
269
+ "%m/%d/%Y", // Month/day/year
270
+ "%m-%d-%Y", // Month-day-year
271
+ "%d-%m-%Y", // Custom format
272
+ "%Y/%m/%d", // Slash-separated date
273
+ "%Y-%m-%dT%H:%M:%S%.f", // ISO 8601 datetime
274
+ "%Y-%m-%d %H:%M:%S", // ISO 8601 with time
275
+ "%d/%m/%Y", // Day/month/year
276
+ "%d.%m.%Y", // Day.month.year
277
+ ];
278
+
279
+ if let Some(fmt) = custom_format {
280
+ formats.push(fmt);
281
+ }
282
+
283
+ // Attempt parsing with each format.
284
+ for &fmt in &formats {
285
+ if let Ok(datetime) = NaiveDateTime::parse_from_str(input, fmt) {
286
+ let microseconds = datetime.and_utc().timestamp_micros() as u32 % 1_000_000;
287
+ let py_date: Py<PyDateTime> = PyDateTime::new(
288
+ py,
289
+ datetime.date().year(),
290
+ datetime.date().month() as u8,
291
+ datetime.date().day() as u8,
292
+ datetime.time().hour() as u8,
293
+ datetime.time().minute() as u8,
294
+ datetime.time().second() as u8,
295
+ microseconds,
296
+ None,
297
+ )?.into();
298
+ return Ok(py_date);
299
+ }
300
+ }
301
+
302
+ // If all attempts fail, raise a ValueError.
303
+ Err(PyValueError::new_err(format!(
304
+ "Unable to parse datetime from '{}'. Tried formats: {:?}",
305
+ input, formats
306
+ )))
307
+ }
308
+
309
+ #[pyfunction]
310
+ #[pyo3(signature = (obj=None))]
311
+ fn to_uuid_obj(py: Python, obj: Option<Py<PyAny>>) -> PyResult<Option<PyObject>> {
312
+ match obj {
313
+ None => Ok(None), // If the object is None, return None
314
+ Some(py_obj) => {
315
+
316
+ let val = py_obj.bind(py);
317
+
318
+ // If the object is already a UUID, return it immediately
319
+ if val.get_type().name()? == "UUID" {
320
+ return Ok(Some(py_obj.into())); // Directly return the Py<PyAny> object
321
+ }
322
+
323
+ // Check if it's a pgproto.UUID (asyncpg's UUID)
324
+ if val.get_type().name()? == "UUID" && val.hasattr("as_text")? {
325
+ if let Ok(uuid_str) = val.call_method0("as_text")?.extract::<String>() {
326
+ if let Ok(parsed_uuid) = Uuid::parse_str(&uuid_str) {
327
+ return Ok(Some(python_uuid(py, parsed_uuid)?));
328
+ }
329
+ }
330
+ }
331
+
332
+ // If the object is callable, call it and use its result
333
+ if val.is_callable() {
334
+ if let Ok(call_result) = val.call0() {
335
+ if let Ok(call_str) = call_result.str()?.to_str() {
336
+ if let Ok(parsed_uuid) = Uuid::parse_str(call_str) {
337
+ return Ok(Some(python_uuid(py, parsed_uuid)?));
338
+ }
339
+ }
340
+ }
341
+ }
342
+
343
+ // Convert to string and try parsing as UUID
344
+ if let Ok(obj_str) = val.str()?.to_str() {
345
+ if let Ok(parsed_uuid) = Uuid::parse_str(obj_str) {
346
+ return Ok(Some(python_uuid(py, parsed_uuid)?));
347
+ }
348
+ }
349
+
350
+ // If all attempts fail, return None
351
+ Ok(None)
352
+ }
353
+ }
354
+ }
355
+
356
+ /// Helper function to create a Python `uuid.UUID` object from a Rust `Uuid`
357
+ fn python_uuid(py: Python, uuid_obj: Uuid) -> PyResult<PyObject> {
358
+ let uuid_mod = py.import("uuid")?;
359
+ let py_uuid = uuid_mod.getattr("UUID")?.call1((uuid_obj.to_string(),))?;
360
+ Ok(py_uuid.into())
361
+ }
362
+
363
+ #[pyfunction]
364
+ #[pyo3(signature = (obj=None))]
365
+ fn to_uuid_str(py: Python, obj: Option<Py<PyAny>>) -> PyResult<Option<PyObject>> {
366
+ match obj {
367
+ None => Ok(None), // If input is None, return None
368
+ Some(py_obj) => {
369
+ let val = py_obj.bind(py);
370
+
371
+ // If it's already a UUID, return it directly (avoid conversion overhead)
372
+ if val.get_type().name()? == "UUID" {
373
+ return Ok(Some(py_obj.into())); // ✅ Fastest path
374
+ }
375
+
376
+ // If it's callable, call it and use its result
377
+ if val.is_callable() {
378
+ let call_result = val.call0()?; // ✅ Only call once
379
+ return to_uuid(py, Some(call_result.extract()?)); // ✅ Recursively process result
380
+ }
381
+
382
+ // If it's a valid UUID string, return it as a string
383
+ if let Ok(obj_str) = val.str()?.to_str() {
384
+ if Uuid::parse_str(obj_str).is_ok() {
385
+ return Ok(Some(PyString::new(py, obj_str).into())); // ✅ Let Python handle conversion
386
+ }
387
+ }
388
+
389
+ // If all attempts fail, return None
390
+ Ok(None)
391
+ }
392
+ }
393
+ }
394
+
395
+ #[pyfunction]
396
+ #[pyo3(signature = (obj=None))]
397
+ fn to_uuid(py: Python, obj: Option<Py<PyAny>>) -> PyResult<Option<PyObject>> {
398
+ match obj {
399
+ None => Ok(None), // If input is None, return None
400
+ Some(py_obj) => {
401
+ let converters = py.import("datamodel.converters")?; // Import Cython module
402
+ let cython_uuid = converters.getattr("to_uuid")?; // Get Cython function
403
+ // Call Cython's to_uuid function and return its result
404
+ let result = cython_uuid.call1((py_obj,))?;
405
+ Ok(Some(result.into()))
406
+ }
407
+ }
408
+ }
409
+
410
+ #[pyfunction]
411
+ #[pyo3(signature = (obj=None))]
412
+ fn to_integer(py: Python, obj: Option<Py<PyAny>>) -> PyResult<Option<PyObject>> {
413
+ match obj {
414
+ None => Ok(None), // If input is None, return None
415
+ Some(py_obj) => {
416
+ // Bind the object to the current GIL.
417
+ let val = py_obj.bind(py);
418
+
419
+ // If the object is already an integer, return it directly.
420
+ if val.is_instance(&PyInt::type_object(py))? {
421
+ return Ok(Some(py_obj.into()));
422
+ }
423
+
424
+ // If the object is a string, attempt to parse it as an integer.
425
+ if val.is_instance(&PyString::type_object(py))? {
426
+ let py_str = val.downcast::<PyString>()?;
427
+ if let Ok(parsed_int) = py_str.to_str()?.parse::<i64>() {
428
+ // Construct a new Python integer by calling the type.
429
+ let py_int_obj = py.get_type::<PyInt>().call1((parsed_int,))?;
430
+ return Ok(Some(py_int_obj.into()));
431
+ } else {
432
+ return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
433
+ format!("Invalid integer string: {}", py_str.to_str()?),
434
+ ));
435
+ }
436
+ }
437
+
438
+ // If it's callable, call it and recursively process its result.
439
+ if val.is_callable() {
440
+ let call_result = val.call0()?; // Call with no arguments.
441
+ return to_integer(py, Some(call_result.extract()?));
442
+ }
443
+
444
+ // Try converting the object to an integer.
445
+ match val.extract::<i64>() {
446
+ Ok(int_value) => {
447
+ // Construct a Python int by calling the Python integer type.
448
+ let py_int_obj = py.get_type::<PyInt>().call1((int_value,))?;
449
+ Ok(Some(py_int_obj.into()))
450
+ }
451
+ Err(_) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
452
+ format!("Invalid conversion to Integer of {}", val.str()?.to_str()?),
453
+ )),
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ #[pyfunction]
460
+ #[pyo3(signature = (obj=None))]
461
+ fn to_float(py: Python, obj: Option<Py<PyAny>>) -> PyResult<Option<PyObject>> {
462
+ match obj {
463
+ None => Ok(None), // If input is None, return None
464
+ Some(py_obj) => {
465
+ let val = py_obj.bind(py); // Bind the object to the Python GIL
466
+
467
+ // If the object is already a float, return it directly.
468
+ if val.get_type().name()? == "float" {
469
+ return Ok(Some(py_obj.into()));
470
+ }
471
+
472
+ // If the object is a string, attempt to parse it as a float.
473
+ if val.is_instance(&PyString::type_object(py))? {
474
+ let py_str = val.downcast::<PyString>()?;
475
+ if let Ok(parsed_float) = py_str.to_str()?.parse::<f64>() {
476
+ // Create a Python float from the Rust f64.
477
+ return Ok(Some(PyFloat::new(py, parsed_float).into()));
478
+ } else {
479
+ return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
480
+ format!("Invalid float string: {}", py_str.to_str()?),
481
+ ));
482
+ }
483
+ }
484
+
485
+ // If it's callable, call it and process its result recursively.
486
+ if val.is_callable() {
487
+ let call_result = val.call0()?; // Call the object with no arguments
488
+ return to_float(py, Some(call_result.extract()?)); // Recursively process result
489
+ }
490
+
491
+ // Try converting the object to an f64.
492
+ match val.extract::<f64>() {
493
+ Ok(float_value) => Ok(Some(PyFloat::new(py, float_value).into())),
494
+ Err(_) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
495
+ format!("Invalid conversion to Float of {}", val.str()?.to_str()?),
496
+ )),
497
+ }
498
+ }
499
+ }
500
+ }
501
+
502
+
503
+ #[pyfunction]
504
+ #[pyo3(signature = (obj=None))]
505
+ fn to_decimal(py: Python, obj: Option<Py<PyAny>>) -> PyResult<Option<PyObject>> {
506
+ match obj {
507
+ None => Ok(None), // If input is None, return None
508
+ Some(py_obj) => {
509
+ let val = py_obj.bind(py); // Bind object in PyO3 0.23.0
510
+
511
+ // If the object is already a Decimal, return it directly
512
+ if val.get_type().name()? == "Decimal" {
513
+ return Ok(Some(py_obj.into())); // ✅ Return existing Decimal
514
+ }
515
+
516
+ // Import Python's `decimal.Decimal`
517
+ let py_decimal = py.import("decimal")?.getattr("Decimal")?;
518
+
519
+ // If the object is a string, attempt to parse it as a Decimal
520
+ if val.is_instance(&PyString::type_object(py))? {
521
+ let py_str = val.downcast::<PyString>()?;
522
+ if let Ok(parsed_decimal) = Decimal::from_str(py_str.to_str()?) {
523
+ return Ok(Some(py_decimal.call1((parsed_decimal.to_string(),))?.into())); // ✅ Convert Rust Decimal to Python Decimal
524
+ } else {
525
+ return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
526
+ format!("Invalid decimal string: {}", py_str.to_str()?),
527
+ ));
528
+ }
529
+ }
530
+
531
+ // If it's callable, call it and process its result
532
+ if val.is_callable() {
533
+ let call_result = val.call0()?; // Only call once
534
+ return to_decimal(py, Some(call_result.extract()?)); // ✅ Recursively process result
535
+ }
536
+
537
+ // Try converting to a Decimal from a float
538
+ match val.extract::<f64>() {
539
+ Ok(float_value) => {
540
+ let rust_decimal = Decimal::from_f64_retain(float_value)
541
+ .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>(
542
+ format!("Invalid conversion to Decimal from float: {}", float_value),
543
+ ))?;
544
+ return Ok(Some(py_decimal.call1((rust_decimal.to_string(),))?.into())); // ✅ Convert Rust Decimal to Python Decimal
545
+ }
546
+ Err(_) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
547
+ format!("Invalid conversion to Decimal of {}", val.str()?.to_str()?),
548
+ )),
549
+ }
550
+ }
551
+ }
552
+ }
553
+
554
+ /// Python module declaration
555
+ #[pymodule]
556
+ fn rs_parsers(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
557
+ m.add_function(wrap_pyfunction!(to_string, m)?)?;
558
+ m.add_function(wrap_pyfunction!(strtobool, m)?)?;
559
+ m.add_function(wrap_pyfunction!(to_boolean, m)?)?;
560
+ m.add_function(wrap_pyfunction!(to_date, m)?)?;
561
+ m.add_function(wrap_pyfunction!(to_datetime, m)?)?;
562
+ m.add_function(wrap_pyfunction!(to_timestamp, m)?)?;
563
+ m.add_function(wrap_pyfunction!(slugify_camelcase, m)?)?;
564
+ m.add_function(wrap_pyfunction!(to_uuid_str, m)?)?;
565
+ m.add_function(wrap_pyfunction!(to_uuid_obj, m)?)?;
566
+ m.add_function(wrap_pyfunction!(to_uuid, m)?)?;
567
+ m.add_function(wrap_pyfunction!(to_integer, m)?)?;
568
+ m.add_function(wrap_pyfunction!(to_float, m)?)?;
569
+ m.add_function(wrap_pyfunction!(to_decimal, m)?)?;
570
+ Ok(())
571
+ }
Binary file
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "rs_validators"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ authors = ["Jesus Lara <jesuslarag@gmail.com>"]
6
+ description = "Functions for value and type validation of datamodel fields."
7
+ license = "MIT"
8
+ repository = "https://github.com/phenobarbital/python-datamodel"
9
+
10
+ [lib]
11
+ name = "rs_validators"
12
+ crate-type = ["cdylib"]
13
+
14
+
15
+ [dependencies]
16
+ pyo3 = { version = "0.23.3", features = ["generate-import-lib", "extension-module"] }
17
+ rayon = "1.5.3"
File without changes
@@ -0,0 +1,9 @@
1
+ from .singleton import Singleton
2
+ from .types import SafeDict, AttrDict, NullDefault
3
+
4
+ __all__ = (
5
+ 'Singleton',
6
+ 'SafeDict',
7
+ 'AttrDict',
8
+ 'NullDefault',
9
+ )