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.
- datamodel/__init__.py +13 -0
- datamodel/abstract.py +383 -0
- datamodel/adaptive/__init__.py +0 -0
- datamodel/adaptive/models.py +598 -0
- datamodel/aliases/__init__.py +26 -0
- datamodel/base.py +180 -0
- datamodel/converters.c +43471 -0
- datamodel/converters.cp313-win32.pyd +0 -0
- datamodel/converters.html +17387 -0
- datamodel/converters.pyx +1489 -0
- datamodel/exceptions.c +13455 -0
- datamodel/exceptions.cp313-win32.pyd +0 -0
- datamodel/exceptions.html +1261 -0
- datamodel/exceptions.pxd +13 -0
- datamodel/exceptions.pyx +50 -0
- datamodel/fields.cp313-win32.pyd +0 -0
- datamodel/fields.cpp +17401 -0
- datamodel/fields.html +3912 -0
- datamodel/fields.pyx +309 -0
- datamodel/functions.cp313-win32.pyd +0 -0
- datamodel/functions.cpp +9068 -0
- datamodel/functions.html +1766 -0
- datamodel/functions.pxd +9 -0
- datamodel/functions.pyx +82 -0
- datamodel/jsonld/__init__.py +45 -0
- datamodel/jsonld/models.py +500 -0
- datamodel/libs/__init__.py +1 -0
- datamodel/libs/mapping.c +15067 -0
- datamodel/libs/mapping.cp313-win32.pyd +0 -0
- datamodel/libs/mapping.html +2618 -0
- datamodel/libs/mapping.pxd +11 -0
- datamodel/libs/mapping.pyx +135 -0
- datamodel/libs/mutables.py +127 -0
- datamodel/models.py +814 -0
- datamodel/parsers/__init__.py +0 -0
- datamodel/parsers/encoders.py +15 -0
- datamodel/parsers/json.cp313-win32.pyd +0 -0
- datamodel/parsers/json.cpp +17004 -0
- datamodel/parsers/json.html +3365 -0
- datamodel/parsers/json.pyx +250 -0
- datamodel/profiler.py +21 -0
- datamodel/py.typed +0 -0
- datamodel/rs_core/Cargo.toml +17 -0
- datamodel/rs_core/src/lib.rs +294 -0
- datamodel/rs_parsers/Cargo.toml +22 -0
- datamodel/rs_parsers/src/lib.rs +571 -0
- datamodel/rs_parsers.cp313-win32.pyd +0 -0
- datamodel/rs_validators/Cargo.toml +17 -0
- datamodel/rs_validators/src/lib.rs +0 -0
- datamodel/typedefs/__init__.py +9 -0
- datamodel/typedefs/singleton.c +9169 -0
- datamodel/typedefs/singleton.cp313-win32.pyd +0 -0
- datamodel/typedefs/singleton.html +629 -0
- datamodel/typedefs/singleton.pxd +9 -0
- datamodel/typedefs/singleton.pyx +24 -0
- datamodel/typedefs/types.c +11716 -0
- datamodel/typedefs/types.cp313-win32.pyd +0 -0
- datamodel/typedefs/types.html +732 -0
- datamodel/typedefs/types.pxd +11 -0
- datamodel/typedefs/types.pyx +39 -0
- datamodel/types.c +7165 -0
- datamodel/types.cp313-win32.pyd +0 -0
- datamodel/types.html +716 -0
- datamodel/types.pyx +100 -0
- datamodel/validation.cp313-win32.pyd +0 -0
- datamodel/validation.cpp +17085 -0
- datamodel/validation.html +4769 -0
- datamodel/validation.pyx +315 -0
- datamodel/version.py +13 -0
- examples/nn/examples.py +311 -0
- examples/nn/stores.py +151 -0
- examples/tests/sp_types.py +294 -0
- examples/tests/speed_dates.py +26 -0
- python_datamodel-0.10.1.dist-info/LICENSE +29 -0
- python_datamodel-0.10.1.dist-info/METADATA +320 -0
- python_datamodel-0.10.1.dist-info/RECORD +78 -0
- python_datamodel-0.10.1.dist-info/WHEEL +5 -0
- 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
|