sqladmin 0.21.0__py3-none-any.whl → 0.23.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.
sqladmin/forms.py CHANGED
@@ -1,6 +1,9 @@
1
+ # mypy: disable-error-code="return-value"
2
+
1
3
  """
2
4
  The converters are from Flask-Admin project.
3
5
  """
6
+
4
7
  from __future__ import annotations
5
8
 
6
9
  import enum
@@ -24,7 +27,6 @@ from sqlalchemy.orm import (
24
27
  sessionmaker,
25
28
  )
26
29
  from sqlalchemy.sql.elements import Label
27
- from sqlalchemy.sql.schema import Column
28
30
  from wtforms import (
29
31
  BooleanField,
30
32
  DecimalField,
@@ -75,11 +77,9 @@ else:
75
77
 
76
78
 
77
79
  class Validator(Protocol):
78
- def __init__(self, *args: Any, **kwargs: Any) -> None:
79
- ... # pragma: no cover
80
+ def __init__(self, *args: Any, **kwargs: Any) -> None: ... # pragma: no cover
80
81
 
81
- def __call__(self, form: Form, field: Field) -> None:
82
- ... # pragma: no cover
82
+ def __call__(self, form: Form, field: Field) -> None: ... # pragma: no cover
83
83
 
84
84
 
85
85
  class ConverterCallable(Protocol):
@@ -88,8 +88,7 @@ class ConverterCallable(Protocol):
88
88
  model: type,
89
89
  prop: MODEL_PROPERTY,
90
90
  kwargs: dict[str, Any],
91
- ) -> UnboundField:
92
- ... # pragma: no cover
91
+ ) -> UnboundField: ... # pragma: no cover
93
92
 
94
93
 
95
94
  T_CC = TypeVar("T_CC", bound=ConverterCallable)
@@ -163,9 +162,14 @@ class ModelConverterBase:
163
162
  return kwargs
164
163
 
165
164
  def _prepare_column(
166
- self, prop: ColumnProperty, form_include_pk: bool, kwargs: dict
165
+ self,
166
+ prop: ColumnProperty,
167
+ form_include_pk: bool,
168
+ kwargs: dict,
167
169
  ) -> Union[dict, None]:
168
- assert len(prop.columns) == 1, "Multiple-column properties not supported"
170
+ if len(prop.columns) != 1:
171
+ raise NotImplementedError("Multiple-column properties are not supported")
172
+
169
173
  column = prop.columns[0]
170
174
 
171
175
  if (column.primary_key or column.foreign_keys) and not form_include_pk:
@@ -210,7 +214,7 @@ class ModelConverterBase:
210
214
  loader: QueryAjaxModelLoader | None = None,
211
215
  ) -> dict:
212
216
  nullable = True
213
- for pair in prop.local_remote_pairs:
217
+ for pair in prop.local_remote_pairs or []:
214
218
  if not pair[0].nullable:
215
219
  nullable = False
216
220
 
@@ -290,9 +294,9 @@ class ModelConverterBase:
290
294
  form_include_pk: bool,
291
295
  label: str | None = None,
292
296
  override: type[Field] | None = None,
293
- form_ajax_refs: dict[str, QueryAjaxModelLoader] = {},
297
+ form_ajax_refs: dict[str, QueryAjaxModelLoader] | None = None,
294
298
  ) -> UnboundField:
295
- loader = form_ajax_refs.get(prop.key)
299
+ loader = (form_ajax_refs or {}).get(prop.key)
296
300
  kwargs = await self._prepare_kwargs(
297
301
  prop=prop,
298
302
  session_maker=session_maker,
@@ -307,7 +311,9 @@ class ModelConverterBase:
307
311
  return None
308
312
 
309
313
  if override is not None:
310
- assert issubclass(override, Field)
314
+ if not issubclass(override, Field):
315
+ raise TypeError("Expected Field, got %s" % type(override))
316
+
311
317
  return override(**kwargs)
312
318
 
313
319
  multiple = (
@@ -317,10 +323,8 @@ class ModelConverterBase:
317
323
  )
318
324
 
319
325
  if loader:
320
- if multiple:
321
- return AjaxSelectMultipleField(loader, **kwargs)
322
- else:
323
- return AjaxSelectField(loader, **kwargs)
326
+ field = AjaxSelectMultipleField if multiple else AjaxSelectField
327
+ return field(loader, **kwargs)
324
328
 
325
329
  converter = self.get_converter(prop=prop)
326
330
  return converter(model=model, prop=prop, kwargs=kwargs)
@@ -333,14 +337,17 @@ class ModelConverter(ModelConverterBase):
333
337
  @staticmethod
334
338
  def _string_common(prop: ColumnProperty) -> list[Validator]:
335
339
  li = []
336
- column: Column = prop.columns[0]
337
- if isinstance(column.type.length, int) and column.type.length:
338
- li.append(validators.Length(max=column.type.length))
340
+ column = prop.columns[0]
341
+ if isinstance(column.type.length, int) and column.type.length: # type: ignore[attr-defined]
342
+ li.append(validators.Length(max=column.type.length)) # type: ignore[attr-defined]
339
343
  return li
340
344
 
341
345
  @converts("String", "CHAR") # includes Unicode
342
346
  def conv_string(
343
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
347
+ self,
348
+ model: type,
349
+ prop: ColumnProperty,
350
+ kwargs: dict[str, Any],
344
351
  ) -> UnboundField:
345
352
  extra_validators = self._string_common(prop)
346
353
  kwargs.setdefault("validators", [])
@@ -349,7 +356,10 @@ class ModelConverter(ModelConverterBase):
349
356
 
350
357
  @converts("Text", "LargeBinary", "Binary") # includes UnicodeText
351
358
  def conv_text(
352
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
359
+ self,
360
+ model: type,
361
+ prop: ColumnProperty,
362
+ kwargs: dict[str, Any],
353
363
  ) -> UnboundField:
354
364
  kwargs.setdefault("validators", [])
355
365
  extra_validators = self._string_common(prop)
@@ -358,7 +368,10 @@ class ModelConverter(ModelConverterBase):
358
368
 
359
369
  @converts("Boolean", "dialects.mssql.base.BIT")
360
370
  def conv_boolean(
361
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
371
+ self,
372
+ model: type,
373
+ prop: ColumnProperty,
374
+ kwargs: dict[str, Any],
362
375
  ) -> UnboundField:
363
376
  if not prop.columns[0].nullable:
364
377
  kwargs.setdefault("render_kw", {})
@@ -372,27 +385,42 @@ class ModelConverter(ModelConverterBase):
372
385
 
373
386
  @converts("Date")
374
387
  def conv_date(
375
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
388
+ self,
389
+ model: type,
390
+ prop: ColumnProperty,
391
+ kwargs: dict[str, Any],
376
392
  ) -> UnboundField:
377
393
  return DateField(**kwargs)
378
394
 
379
395
  @converts("Time")
380
396
  def conv_time(
381
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
397
+ self,
398
+ model: type,
399
+ prop: ColumnProperty,
400
+ kwargs: dict[str, Any],
382
401
  ) -> UnboundField:
383
402
  return TimeField(**kwargs)
384
403
 
385
404
  @converts("DateTime")
386
405
  def conv_datetime(
387
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
406
+ self,
407
+ model: type,
408
+ prop: ColumnProperty,
409
+ kwargs: dict[str, Any],
388
410
  ) -> UnboundField:
389
411
  return DateTimeField(**kwargs)
390
412
 
391
413
  @converts("Enum")
392
414
  def conv_enum(
393
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
415
+ self,
416
+ model: type,
417
+ prop: ColumnProperty,
418
+ kwargs: dict[str, Any],
394
419
  ) -> UnboundField:
395
- available_choices = [(e, e) for e in prop.columns[0].type.enums]
420
+ available_choices = [
421
+ (e, e)
422
+ for e in prop.columns[0].type.enums # type: ignore[attr-defined]
423
+ ]
396
424
  accepted_values = [choice[0] for choice in available_choices]
397
425
 
398
426
  if prop.columns[0].nullable:
@@ -410,13 +438,19 @@ class ModelConverter(ModelConverterBase):
410
438
 
411
439
  @converts("Integer") # includes BigInteger and SmallInteger
412
440
  def conv_integer(
413
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
441
+ self,
442
+ model: type,
443
+ prop: ColumnProperty,
444
+ kwargs: dict[str, Any],
414
445
  ) -> UnboundField:
415
446
  return IntegerField(**kwargs)
416
447
 
417
448
  @converts("Numeric") # includes DECIMAL, Float/FLOAT, REAL, and DOUBLE
418
449
  def conv_decimal(
419
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
450
+ self,
451
+ model: type,
452
+ prop: ColumnProperty,
453
+ kwargs: dict[str, Any],
420
454
  ) -> UnboundField:
421
455
  # override default decimal places limit, use database defaults instead
422
456
  kwargs.setdefault("places", None)
@@ -424,13 +458,19 @@ class ModelConverter(ModelConverterBase):
424
458
 
425
459
  @converts("JSON", "JSONB")
426
460
  def conv_json(
427
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
461
+ self,
462
+ model: type,
463
+ prop: ColumnProperty,
464
+ kwargs: dict[str, Any],
428
465
  ) -> UnboundField:
429
466
  return JSONField(**kwargs)
430
467
 
431
468
  @converts("Interval")
432
469
  def conv_interval(
433
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
470
+ self,
471
+ model: type,
472
+ prop: ColumnProperty,
473
+ kwargs: dict[str, Any],
434
474
  ) -> UnboundField:
435
475
  kwargs["render_kw"]["placeholder"] = "Like: 1 day 1:25:33.652"
436
476
  return IntervalField(**kwargs)
@@ -441,7 +481,10 @@ class ModelConverter(ModelConverterBase):
441
481
  "sqlalchemy_utils.types.ip_address.IPAddressType",
442
482
  )
443
483
  def conv_ip_address(
444
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
484
+ self,
485
+ model: type,
486
+ prop: ColumnProperty,
487
+ kwargs: dict[str, Any],
445
488
  ) -> UnboundField:
446
489
  kwargs.setdefault("validators", [])
447
490
  kwargs["validators"].append(validators.IPAddress(ipv4=True, ipv6=True))
@@ -452,7 +495,10 @@ class ModelConverter(ModelConverterBase):
452
495
  "sqlalchemy.dialects.postgresql.types.MACADDR",
453
496
  )
454
497
  def conv_mac_address(
455
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
498
+ self,
499
+ model: type,
500
+ prop: ColumnProperty,
501
+ kwargs: dict[str, Any],
456
502
  ) -> UnboundField:
457
503
  kwargs.setdefault("validators", [])
458
504
  kwargs["validators"].append(validators.MacAddress())
@@ -465,23 +511,33 @@ class ModelConverter(ModelConverterBase):
465
511
  "sqlalchemy_utils.types.uuid.UUIDType",
466
512
  )
467
513
  def conv_uuid(
468
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
514
+ self,
515
+ model: type,
516
+ prop: ColumnProperty,
517
+ kwargs: dict[str, Any],
469
518
  ) -> UnboundField:
470
519
  kwargs.setdefault("validators", [])
471
520
  kwargs["validators"].append(validators.UUID())
472
521
  return StringField(**kwargs)
473
522
 
474
523
  @converts(
475
- "sqlalchemy.dialects.postgresql.base.ARRAY", "sqlalchemy.sql.sqltypes.ARRAY"
524
+ "sqlalchemy.dialects.postgresql.base.ARRAY",
525
+ "sqlalchemy.sql.sqltypes.ARRAY",
476
526
  )
477
- def conv_ARRAY(
478
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
527
+ def conv_array(
528
+ self,
529
+ model: type,
530
+ prop: ColumnProperty,
531
+ kwargs: dict[str, Any],
479
532
  ) -> UnboundField:
480
533
  return Select2TagsField(**kwargs)
481
534
 
482
535
  @converts("sqlalchemy_utils.types.email.EmailType")
483
536
  def conv_email(
484
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
537
+ self,
538
+ model: type,
539
+ prop: ColumnProperty,
540
+ kwargs: dict[str, Any],
485
541
  ) -> UnboundField:
486
542
  kwargs.setdefault("validators", [])
487
543
  kwargs["validators"].append(validators.Email())
@@ -489,7 +545,10 @@ class ModelConverter(ModelConverterBase):
489
545
 
490
546
  @converts("sqlalchemy_utils.types.url.URLType")
491
547
  def conv_url(
492
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
548
+ self,
549
+ model: type,
550
+ prop: ColumnProperty,
551
+ kwargs: dict[str, Any],
493
552
  ) -> UnboundField:
494
553
  kwargs.setdefault("validators", [])
495
554
  kwargs["validators"].append(validators.URL())
@@ -497,7 +556,10 @@ class ModelConverter(ModelConverterBase):
497
556
 
498
557
  @converts("sqlalchemy_utils.types.currency.CurrencyType")
499
558
  def conv_currency(
500
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
559
+ self,
560
+ model: type,
561
+ prop: ColumnProperty,
562
+ kwargs: dict[str, Any],
501
563
  ) -> UnboundField:
502
564
  kwargs.setdefault("validators", [])
503
565
  kwargs["validators"].append(CurrencyValidator())
@@ -505,17 +567,23 @@ class ModelConverter(ModelConverterBase):
505
567
 
506
568
  @converts("sqlalchemy_utils.types.timezone.TimezoneType")
507
569
  def conv_timezone(
508
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
570
+ self,
571
+ model: type,
572
+ prop: ColumnProperty,
573
+ kwargs: dict[str, Any],
509
574
  ) -> UnboundField:
510
575
  kwargs.setdefault("validators", [])
511
576
  kwargs["validators"].append(
512
- TimezoneValidator(coerce_function=prop.columns[0].type._coerce)
577
+ TimezoneValidator(coerce_function=prop.columns[0].type._coerce) # type: ignore[attr-defined]
513
578
  )
514
579
  return StringField(**kwargs)
515
580
 
516
581
  @converts("sqlalchemy_utils.types.phone_number.PhoneNumberType")
517
582
  def conv_phone_number(
518
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
583
+ self,
584
+ model: type,
585
+ prop: ColumnProperty,
586
+ kwargs: dict[str, Any],
519
587
  ) -> UnboundField:
520
588
  kwargs.setdefault("validators", [])
521
589
  kwargs["validators"].append(PhoneNumberValidator())
@@ -523,7 +591,10 @@ class ModelConverter(ModelConverterBase):
523
591
 
524
592
  @converts("sqlalchemy_utils.types.color.ColorType")
525
593
  def conv_color(
526
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
594
+ self,
595
+ model: type,
596
+ prop: ColumnProperty,
597
+ kwargs: dict[str, Any],
527
598
  ) -> UnboundField:
528
599
  kwargs.setdefault("validators", [])
529
600
  kwargs["validators"].append(ColorValidator())
@@ -532,7 +603,10 @@ class ModelConverter(ModelConverterBase):
532
603
  @converts("sqlalchemy_utils.types.choice.ChoiceType")
533
604
  @no_type_check
534
605
  def convert_choice_type(
535
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
606
+ self,
607
+ model: type,
608
+ prop: ColumnProperty,
609
+ kwargs: dict[str, Any],
536
610
  ) -> UnboundField:
537
611
  available_choices = []
538
612
  column = prop.columns[0]
@@ -561,32 +635,47 @@ class ModelConverter(ModelConverterBase):
561
635
 
562
636
  @converts("fastapi_storages.integrations.sqlalchemy.FileType")
563
637
  def conv_file(
564
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
638
+ self,
639
+ model: type,
640
+ prop: ColumnProperty,
641
+ kwargs: dict[str, Any],
565
642
  ) -> UnboundField:
566
643
  return FileField(**kwargs)
567
644
 
568
645
  @converts("fastapi_storages.integrations.sqlalchemy.ImageType")
569
646
  def conv_image(
570
- self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
647
+ self,
648
+ model: type,
649
+ prop: ColumnProperty,
650
+ kwargs: dict[str, Any],
571
651
  ) -> UnboundField:
572
652
  return FileField(**kwargs)
573
653
 
574
654
  @converts("ONETOONE")
575
655
  def conv_one_to_one(
576
- self, model: type, prop: RelationshipProperty, kwargs: dict[str, Any]
656
+ self,
657
+ model: type,
658
+ prop: RelationshipProperty,
659
+ kwargs: dict[str, Any],
577
660
  ) -> UnboundField:
578
661
  kwargs["allow_blank"] = True
579
662
  return QuerySelectField(**kwargs)
580
663
 
581
664
  @converts("MANYTOONE")
582
665
  def conv_many_to_one(
583
- self, model: type, prop: RelationshipProperty, kwargs: dict[str, Any]
666
+ self,
667
+ model: type,
668
+ prop: RelationshipProperty,
669
+ kwargs: dict[str, Any],
584
670
  ) -> UnboundField:
585
671
  return QuerySelectField(**kwargs)
586
672
 
587
673
  @converts("MANYTOMANY", "ONETOMANY")
588
674
  def conv_many_to_many(
589
- self, model: type, prop: RelationshipProperty, kwargs: dict[str, Any]
675
+ self,
676
+ model: type,
677
+ prop: RelationshipProperty,
678
+ kwargs: dict[str, Any],
590
679
  ) -> UnboundField:
591
680
  return QuerySelectMultipleField(**kwargs)
592
681
 
sqladmin/helpers.py CHANGED
@@ -6,7 +6,7 @@ import os
6
6
  import re
7
7
  import unicodedata
8
8
  from abc import ABC, abstractmethod
9
- from datetime import timedelta
9
+ from datetime import date, datetime, time, timedelta
10
10
  from typing import (
11
11
  Any,
12
12
  AsyncGenerator,
@@ -123,7 +123,11 @@ def secure_filename(filename: str) -> str:
123
123
  if (
124
124
  os.name == "nt"
125
125
  and filename
126
- and filename.split(".")[0].upper() in _windows_device_files
126
+ and filename.split(
127
+ ".",
128
+ maxsplit=1,
129
+ )[0].upper()
130
+ in _windows_device_files
127
131
  ):
128
132
  filename = f"_{filename}" # pragma: no cover
129
133
 
@@ -226,13 +230,21 @@ def object_identifier_values(id_string: str, model: Any) -> tuple:
226
230
  pks = get_primary_keys(model)
227
231
  for pk, part in zip(pks, _object_identifier_parts(id_string, model)):
228
232
  type_ = get_column_python_type(pk)
229
- value = False if type_ is bool and part == "False" else type_(part)
233
+ value: Any
234
+ if issubclass(type_, (date, datetime, time)):
235
+ value = type_.fromisoformat(part)
236
+ elif issubclass(type_, bool):
237
+ value = False if part == "False" else type_(part)
238
+ else:
239
+ value = type_(part) # type: ignore [call-arg]
230
240
  values.append(value)
231
241
  return tuple(values)
232
242
 
233
243
 
234
244
  def get_direction(prop: MODEL_PROPERTY) -> str:
235
- assert isinstance(prop, RelationshipProperty)
245
+ if not isinstance(prop, RelationshipProperty):
246
+ raise TypeError("Expected RelationshipProperty, got %s" % type(prop))
247
+
236
248
  name = prop.direction.name
237
249
  if name == "ONETOMANY" and not prop.uselist:
238
250
  return "ONETOONE"
@@ -279,10 +291,11 @@ def parse_interval(value: str) -> timedelta | None:
279
291
  def is_falsy_value(value: Any) -> bool:
280
292
  if value is None:
281
293
  return True
282
- elif not value and isinstance(value, str):
294
+
295
+ if not value and isinstance(value, str):
283
296
  return True
284
- else:
285
- return False
297
+
298
+ return False
286
299
 
287
300
 
288
301
  def choice_type_coerce_factory(type_: Any) -> Callable[[Any], Any]: