DataOpsHub 6.2.0__py3-none-any.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.
@@ -0,0 +1,513 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Full-Stack Form Field Validation Module
6
+
7
+ This module provides comprehensive validation functionality for form fields in full-stack applications.
8
+ It combines backend-oriented validation with frontend-friendly features for user registration and other forms.
9
+ """
10
+
11
+ #----------------#
12
+ # Import modules #
13
+ #----------------#
14
+
15
+ from email_validator import validate_email, EmailNotValidError
16
+
17
+ #------------------------#
18
+ # Import project modules #
19
+ #------------------------#
20
+
21
+ from DataOpsHub import (
22
+ INVALID_AGE_ERROR,
23
+ INVALID_DATE_FORMAT_ERROR,
24
+ INVALID_DATE_RANGE_ERROR,
25
+ INVALID_DATE_RANGE_FORMAT_ERROR,
26
+ INVALID_EMAIL_ERROR,
27
+ INVALID_FIRST_NAME_ERROR,
28
+ INVALID_FIRST_SURNAME_ERROR,
29
+ INVALID_ID_FORMAT_ERROR,
30
+ INVALID_ID_TYPE_ERROR,
31
+ INVALID_PASSWORD_ERROR,
32
+ INVALID_SECOND_SURNAME_ERROR,
33
+ INVALID_TEL_ERROR,
34
+ INVALID_TEL_SEARCH_VALUE,
35
+ MISSING_DATE_VALUES_ERROR,
36
+ USER_EXISTS_ERROR
37
+ )
38
+
39
+ from pygenutils.strings.string_handler import find_substring_index
40
+ from pygenutils.time_handling.time_formatters import parse_time_string
41
+
42
+ #------------------#
43
+ # Define functions #
44
+ #------------------#
45
+
46
+ # General validation functions #
47
+ #------------------------------#
48
+
49
+ def validate_field(field_name, field_value, reg_exp_dict, err_str_list):
50
+ """
51
+ Validates a field based on the provided regular expression and error string.
52
+
53
+ Parameters
54
+ ----------
55
+ field_name : str
56
+ The name of the field being validated.
57
+ field_value : str
58
+ The value of the field to validate.
59
+ reg_exp_dict : dict
60
+ A dictionary of field names and their corresponding regular expressions.
61
+ err_str_list : list
62
+ A list of error messages corresponding to the fields.
63
+
64
+ Returns
65
+ -------
66
+ dict
67
+ A dictionary with `has_error` (bool) and `error_msg` (str).
68
+ """
69
+ # Special handling for complex validations
70
+ if field_name == FIELD_NAME_LIST_ENG[0]: # email
71
+ return validate_email_field(field_value)
72
+ elif field_name == FIELD_NAME_LIST_ENG[-1]: # age
73
+ return validate_age_field(field_value)
74
+ elif field_name == 'id': # ID validation
75
+ return validate_id_field(field_value, INVALID_ID_FORMAT_ERROR, INVALID_ID_TYPE_ERROR)
76
+
77
+ # Default validation using regular expressions
78
+ has_error = find_substring_index(
79
+ field_value.strip() if field_name != FIELD_NAME_LIST_ENG[4] else field_value,
80
+ reg_exp_dict[field_name],
81
+ advanced_search=True
82
+ ) == -1
83
+
84
+ error_msg = err_str_list[FIELD_NAME_LIST_ENG.index(field_name)] if has_error else ""
85
+ return {"has_error": has_error, "error_msg": error_msg}
86
+
87
+
88
+ def generate_error_dict(request_form, reg_exp_dict, err_str_list):
89
+ """
90
+ Generates an error dictionary for all form fields.
91
+
92
+ Parameters
93
+ ----------
94
+ request_form : dict
95
+ A dictionary containing form field data (usually from Flask `request.form`).
96
+ reg_exp_dict : dict
97
+ A dictionary of field names and their corresponding regular expressions.
98
+ err_str_list : list
99
+ A list of error messages corresponding to the fields.
100
+
101
+ Returns
102
+ -------
103
+ dict
104
+ A dictionary where each key is a field name and each value contains
105
+ `has_error` (bool) and `error_msg` (str).
106
+ """
107
+ return {
108
+ field_name: validate_field(
109
+ field_name,
110
+ request_form.get(field_name),
111
+ reg_exp_dict,
112
+ err_str_list,
113
+ )
114
+ for field_name in FIELD_NAME_LIST_ENG
115
+ }
116
+
117
+
118
+ def generate_error_dict_nonempty(request_form, reg_exp_dict, err_str_list, exclude_fields=None):
119
+ """
120
+ Generates an error dictionary for all non-empty form fields.
121
+
122
+ Parameters
123
+ ----------
124
+ request_form : dict
125
+ A dictionary containing form field data (usually from Flask `request.form`).
126
+ reg_exp_dict : dict
127
+ A dictionary of field names and their corresponding regular expressions.
128
+ err_str_list : list
129
+ A list of error messages corresponding to the fields.
130
+ exclude_fields : list
131
+ A list of field names to exclude from validation.
132
+
133
+ Returns
134
+ -------
135
+ dict
136
+ A dictionary where each key is a field name and each value contains
137
+ `has_error` (bool) and `error_msg` (str).
138
+ """
139
+ if exclude_fields is None:
140
+ exclude_fields = []
141
+ elif exclude_fields and not isinstance(exclude_fields, list):
142
+ exclude_fields = [exclude_fields]
143
+
144
+ err_dict = {
145
+ field_name: validate_field(
146
+ field_name,
147
+ request_form.get(field_name),
148
+ reg_exp_dict,
149
+ err_str_list,
150
+ )
151
+ for field_name in FIELD_NAME_LIST_ENG
152
+ if request_form.get(field_name) and field_name not in exclude_fields
153
+ }
154
+ return err_dict
155
+
156
+
157
+ # Specific validation functions #
158
+ #------------------------------#
159
+
160
+ def verify_spanish_dni(dni: str) -> bool:
161
+ """
162
+ Verify the validity of a Spanish DNI (Documento Nacional de Identidad).
163
+
164
+ Parameters
165
+ ----------
166
+ dni : str
167
+ The DNI (ID card) to be verified.
168
+
169
+ Returns
170
+ -------
171
+ bool
172
+ True if the DNI is valid, False otherwise.
173
+ """
174
+ if len(dni) != 9:
175
+ return False
176
+
177
+ digits = dni[:-1]
178
+ control_letter = dni[-1].upper()
179
+
180
+ if not digits.isdigit():
181
+ return False
182
+
183
+ letters = "TRWAGMYFPDXBNJZSQVHLCKE"
184
+ calculated_letter = letters[int(digits) % 23]
185
+
186
+ return calculated_letter == control_letter
187
+
188
+ def validate_id_field(value, invalid_val_err_str, empty_val_err_str):
189
+ """
190
+ Validate if the ID follows the required format (3-10 characters, alphanumeric with hyphens).
191
+
192
+ Parameters
193
+ ----------
194
+ value : str
195
+ The ID value to validate.
196
+ invalid_val_err_str : str
197
+ The error message for invalid ID format.
198
+ empty_val_err_str : str
199
+ The error message for empty ID values.
200
+
201
+ Returns
202
+ -------
203
+ dict
204
+ A dictionary with the error message, if any, stored under the key `error_msg` (str).
205
+ """
206
+ try:
207
+ if not isinstance(value, str):
208
+ return {"error_msg": empty_val_err_str}
209
+
210
+ # First check if it's a Spanish DNI
211
+ if len(value) == 9 and value[-1].isalpha():
212
+ if not verify_spanish_dni(value):
213
+ return {"error_msg": invalid_val_err_str}
214
+ return {"error_msg": ""}
215
+
216
+ # If not a DNI, check the standard ID pattern
217
+ pattern_match_index = find_substring_index(value, ID_PATTERN, advanced_search=True)
218
+ if pattern_match_index == -1:
219
+ return {"error_msg": invalid_val_err_str}
220
+
221
+ return {"error_msg": ""}
222
+ except (ValueError, TypeError):
223
+ return {"error_msg": empty_val_err_str}
224
+
225
+ def validate_date_range(min_value, max_value):
226
+ """
227
+ Validate date range values, always including both edges of the range.
228
+
229
+ Parameters
230
+ ----------
231
+ min_value : str
232
+ Start date in format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM'.
233
+ If only date is provided, time defaults to 00:00.
234
+ max_value : str
235
+ End date in format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM'.
236
+ If only date is provided, time defaults to 23:59.
237
+
238
+ Returns
239
+ -------
240
+ dict
241
+ A dictionary with the error message, if any, stored under the key `error_msg` (str).
242
+ If multiple errors are found, they will be combined with semicolons.
243
+ """
244
+ errors = []
245
+ min_date = None
246
+ max_date = None
247
+ min_value_error = False
248
+ max_value_error = False
249
+
250
+ # Try parsing min_value
251
+ try:
252
+ # Try parsing with time first
253
+ try:
254
+ min_date = parse_time_string(min_value, '%Y-%m-%d %H:%M')
255
+ except ValueError:
256
+ # If that fails, try date-only format and set time to 00:00
257
+ try:
258
+ min_date = parse_time_string(min_value, '%Y-%m-%d')
259
+ min_date = min_date.replace(hour=0, minute=0)
260
+ except ValueError:
261
+ min_value_error = True
262
+ except Exception:
263
+ min_value_error = True
264
+
265
+ # Try parsing max_value
266
+ try:
267
+ # Try parsing with time first
268
+ try:
269
+ max_date = parse_time_string(max_value, '%Y-%m-%d %H:%M')
270
+ except ValueError:
271
+ # If that fails, try date-only format and set time to 23:59
272
+ try:
273
+ max_date = parse_time_string(max_value, '%Y-%m-%d')
274
+ max_date = max_date.replace(hour=23, minute=59)
275
+ except ValueError:
276
+ max_value_error = True
277
+ except Exception:
278
+ max_value_error = True
279
+
280
+ # Create consolidated error messages for date format issues
281
+ if min_value_error and max_value_error:
282
+ errors.append(f"Invalid date format for min_value and max_value. {INVALID_DATE_FORMAT_ERROR}")
283
+ elif min_value_error:
284
+ errors.append(f"Invalid date format for min_value: '{min_value}'. {INVALID_DATE_FORMAT_ERROR}")
285
+ elif max_value_error:
286
+ errors.append(f"Invalid date format for max_value: '{max_value}'. {INVALID_DATE_FORMAT_ERROR}")
287
+
288
+ # If both dates are valid, check if min_date is greater than max_date
289
+ if min_date and max_date and min_date > max_date:
290
+ errors.append(INVALID_DATE_RANGE_ERROR)
291
+
292
+ # Return all errors at once if any were found
293
+ if errors:
294
+ return {"error_msg": "; ".join(errors)}
295
+
296
+ return {"error_msg": ""}
297
+
298
+ def validate_email_field(email):
299
+ """
300
+ Validates and normalizes an email address.
301
+
302
+ Parameters
303
+ ----------
304
+ email: str
305
+ The email address to validate.
306
+
307
+ Returns
308
+ -------
309
+ dict
310
+ A dictionary with `has_error` (bool) and `error_msg` (str).
311
+ """
312
+ try:
313
+ validate_email(email)
314
+ return {"has_error": False, "error_msg": ""}
315
+ except EmailNotValidError as e:
316
+ return {"has_error": True, "error_msg": f"{INVALID_EMAIL_ERROR}: {str(e)}"}
317
+
318
+ def validate_age_field(age):
319
+ """
320
+ Validate if the age is within the range [14-120].
321
+
322
+ Parameters
323
+ ----------
324
+ age : str
325
+ The age input value as a string.
326
+
327
+ Returns
328
+ -------
329
+ dict
330
+ A dictionary with `has_error` (bool) and `error_msg` (str).
331
+ """
332
+ if not (age.isdigit() and 14 <= int(age) <= 110):
333
+ return {"has_error": True, "error_msg": INVALID_AGE_ERROR}
334
+ return {"has_error": False, "error_msg": ""}
335
+
336
+ # Hybrid validation functions #
337
+ #----------------------------#
338
+
339
+ def validate_form_with_json(form_data, json_data=None):
340
+ """
341
+ Validates both form data and JSON data in a full-stack context.
342
+
343
+ This function combines the backend-oriented JSON validation with
344
+ the frontend-oriented form validation.
345
+
346
+ Parameters
347
+ ----------
348
+ form_data : dict
349
+ Form data from a web form (e.g., Flask request.form).
350
+ json_data : dict, optional
351
+ JSON data for API validation.
352
+
353
+ Returns
354
+ -------
355
+ dict
356
+ A dictionary with validation results for both form and JSON data.
357
+ """
358
+ results = {
359
+ "form_validation": {},
360
+ "json_validation": {}
361
+ }
362
+
363
+ # Validate form data if provided
364
+ if form_data:
365
+ results["form_validation"] = generate_error_dict(
366
+ form_data,
367
+ REG_EXP_DICT,
368
+ ERR_STR_LIST
369
+ )
370
+
371
+ # Validate JSON data if provided
372
+ if json_data:
373
+ results["json_validation"] = generate_json_validation_response(json_data)
374
+
375
+ return results
376
+
377
+ def generate_json_validation_response(json_data):
378
+ """
379
+ Generates a validation response for medical data fields from JSON input.
380
+
381
+ Parameters
382
+ ----------
383
+ json_data : dict
384
+ The JSON data containing medical measurements to validate.
385
+ Expected fields are:
386
+ - id (str)
387
+ - created_at (dict with min_value and max_value)
388
+
389
+ Returns
390
+ -------
391
+ dict
392
+ JSON-formatted validation response for each field:
393
+ {
394
+ "field_name": {
395
+ "error_msg": str
396
+ }
397
+ }
398
+ """
399
+ validation_results = {}
400
+
401
+ for field_name, field_specs_dict in json_data.items():
402
+ if field_name == 'created_at':
403
+ # Validate date range format and values
404
+ if not isinstance(field_specs_dict, dict):
405
+ validation_results[field_name] = {"error_msg": INVALID_DATE_RANGE_FORMAT_ERROR}
406
+ continue
407
+
408
+ min_value = field_specs_dict.get("min_value")
409
+ max_value = field_specs_dict.get("max_value")
410
+
411
+ if not min_value or not max_value:
412
+ validation_results[field_name] = {"error_msg": MISSING_DATE_VALUES_ERROR}
413
+ continue
414
+
415
+ validation_results[field_name] = validate_date_range(min_value, max_value)
416
+ elif field_name == 'id':
417
+ validation_results[field_name] = validate_id_field(field_specs_dict, INVALID_ID_FORMAT_ERROR, INVALID_ID_TYPE_ERROR)
418
+
419
+ return validation_results
420
+
421
+ #--------------------------#
422
+ # Parameters and constants #
423
+ #--------------------------#
424
+
425
+ # Field name keywords #
426
+ #--------------------#
427
+
428
+ # User registration fields
429
+ FIELD_NAME_LIST_ENG = [
430
+ "email",
431
+ "first_name",
432
+ "first_surname",
433
+ "second_surname",
434
+ "password",
435
+ "tel",
436
+ "age"
437
+ ]
438
+
439
+ FIELD_NAME_LIST_ESP = [
440
+ "Correo electrónico",
441
+ "Nombre",
442
+ "Primer apellido",
443
+ "Segundo apellido",
444
+ "Contraseña",
445
+ "Teléfono",
446
+ "Edad"
447
+ ]
448
+
449
+ DROPDOWN_ID_LIST = [f"{field}_dropdown" for field in FIELD_NAME_LIST_ENG]
450
+ FIELD_NAME_SEARCH_LIST = [f"{field}_search_value" for field in FIELD_NAME_LIST_ENG]
451
+
452
+ # Fields that can be used for filtering (WHERE clauses)
453
+ FILTERABLE_FIELDS = ["id", "created_at"]
454
+
455
+ # Fields to display in the response (SELECT clauses)
456
+ DISPLAYABLE_FIELDS = ["hl7_v2"]
457
+
458
+ # Pretty names for documentation
459
+ FIELD_NAME_LIST_PRETTY = [
460
+ "ID",
461
+ "Age",
462
+ "Body Temperature",
463
+ "Blood Pressure",
464
+ "Leucocyte Count",
465
+ "Neutrophil Count",
466
+ "Lymphocyte Count",
467
+ "Monocyte Count",
468
+ "User Registration Date",
469
+ "HL7 v2.X"
470
+ ]
471
+
472
+ # ID validation regex pattern
473
+ ID_PATTERN = r'^P[0-9]{3}$'
474
+
475
+ # Create lists of error messages for backward compatibility
476
+ ERR_STR_LIST = [
477
+ INVALID_EMAIL_ERROR,
478
+ INVALID_FIRST_NAME_ERROR,
479
+ INVALID_FIRST_SURNAME_ERROR,
480
+ INVALID_SECOND_SURNAME_ERROR,
481
+ INVALID_PASSWORD_ERROR,
482
+ INVALID_TEL_ERROR,
483
+ INVALID_AGE_ERROR,
484
+ USER_EXISTS_ERROR
485
+ ]
486
+
487
+ ERR_STR_LIST_SEARCH = ERR_STR_LIST.copy()
488
+ INVALID_TEL_SEARCH_VALUE = INVALID_TEL_SEARCH_VALUE
489
+
490
+ # Regular expressions #
491
+ #---------------------#
492
+
493
+ REG_EXP_DICT = {
494
+ FIELD_NAME_LIST_ENG[1]: r"^[a-zá-úà-ùâ-ûä-üA-ZÁ-ÚÀ-ÙÂ-ÛÄ-ÜçñÇÑ\s-]{3,}$",
495
+ FIELD_NAME_LIST_ENG[2]: r"^[a-zá-úà-ùâ-ûä-üA-ZÁ-ÚÀ-ÙÂ-ÛÄ-ÜçñÇÑ\s'-]{3,}$",
496
+ FIELD_NAME_LIST_ENG[3]: r"^[a-zá-úà-ùâ-ûä-üA-ZÁ-ÚÀ-ÙÂ-ÛÄ-ÜçñÇÑ\s'-]{3,}$",
497
+ FIELD_NAME_LIST_ENG[4]: r"^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[_\W]).+$",
498
+ FIELD_NAME_LIST_ENG[5]: r"^(\+[1-9][0-9]{1,2} [0-9]{3} [0-9]{3} [0-9]{3}|\+[1-9]{1}-[0-9]{3} [0-9]{3} [0-9]{3} [0-9]{3}|[0-9]{3} [0-9]{3} [0-9]{3})$",
499
+ }
500
+
501
+ REG_EXP_DICT_SEARCH = {
502
+ FIELD_NAME_LIST_ENG[1]: r"^[a-zá-úà-ùâ-ûä-üA-ZÁ-ÚÀ-ÙÂ-ÛÄ-ÜçñÇÑ\s%-]*$",
503
+ FIELD_NAME_LIST_ENG[2]: r"^[a-zá-úà-ùâ-ûä-üA-ZÁ-ÚÀ-ÙÂ-ÛÄ-ÜçñÇÑ\s%-]*$",
504
+ FIELD_NAME_LIST_ENG[3]: r"^[a-zá-úà-ùâ-ûä-üA-ZÁ-ÚÀ-ÙÂ-ÛÄ-ÜçñÇÑ\s%-]*$",
505
+ FIELD_NAME_LIST_ENG[5]: r"^\+[0-9\s]*$",
506
+ }
507
+
508
+ # Validation mapping #
509
+ FIELD_VALIDATORS = {
510
+ "id": lambda x: validate_id_field(x, INVALID_ID_FORMAT_ERROR, INVALID_ID_TYPE_ERROR),
511
+ "email": validate_email_field,
512
+ "age": validate_age_field
513
+ }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # DataOpsHub/databases/__init__.py
5
+
6
+ # Define what should be available when using 'from DataOpsHub.databases import *'
7
+ __all__ = [
8
+ 'database_handler',
9
+ 'upload_data'
10
+ ]