sqladmin 0.22.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/helpers.py CHANGED
@@ -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
 
@@ -238,7 +242,9 @@ def object_identifier_values(id_string: str, model: Any) -> tuple:
238
242
 
239
243
 
240
244
  def get_direction(prop: MODEL_PROPERTY) -> str:
241
- assert isinstance(prop, RelationshipProperty)
245
+ if not isinstance(prop, RelationshipProperty):
246
+ raise TypeError("Expected RelationshipProperty, got %s" % type(prop))
247
+
242
248
  name = prop.direction.name
243
249
  if name == "ONETOMANY" and not prop.uselist:
244
250
  return "ONETOONE"
@@ -285,10 +291,11 @@ def parse_interval(value: str) -> timedelta | None:
285
291
  def is_falsy_value(value: Any) -> bool:
286
292
  if value is None:
287
293
  return True
288
- elif not value and isinstance(value, str):
294
+
295
+ if not value and isinstance(value, str):
289
296
  return True
290
- else:
291
- return False
297
+
298
+ return False
292
299
 
293
300
 
294
301
  def choice_type_coerce_factory(type_: Any) -> Callable[[Any], Any]:
sqladmin/models.py CHANGED
@@ -60,10 +60,11 @@ from sqladmin.helpers import (
60
60
 
61
61
  # stream_to_csv,
62
62
  from sqladmin.pagination import Pagination
63
+ from sqladmin.pretty_export import PrettyExport
63
64
  from sqladmin.templating import Jinja2Templates
64
65
 
65
66
  if TYPE_CHECKING:
66
- from sqlalchemy.ext.asyncio import async_sessionmaker
67
+ from sqlalchemy.ext.asyncio import async_sessionmaker # type: ignore[attr-defined]
67
68
 
68
69
  from sqladmin.application import BaseAdmin
69
70
 
@@ -82,8 +83,8 @@ class ModelViewMeta(type):
82
83
  """
83
84
 
84
85
  @no_type_check
85
- def __new__(mcls, name, bases, attrs: dict, **kwargs: Any):
86
- cls: Type["ModelView"] = super().__new__(mcls, name, bases, attrs)
86
+ def __new__(mcs, name, bases, attrs: dict, **kwargs: Any):
87
+ cls: Type["ModelView"] = super().__new__(mcs, name, bases, attrs)
87
88
 
88
89
  model = kwargs.get("model")
89
90
 
@@ -92,10 +93,10 @@ class ModelViewMeta(type):
92
93
 
93
94
  try:
94
95
  inspect(model)
95
- except NoInspectionAvailable:
96
+ except NoInspectionAvailable as exc:
96
97
  raise InvalidModelError(
97
98
  f"Class {model.__name__} is not a SQLAlchemy model."
98
- )
99
+ ) from exc
99
100
 
100
101
  cls.pk_columns = get_primary_keys(model)
101
102
  cls.identity = slugify_class_name(model.__name__)
@@ -104,21 +105,19 @@ class ModelViewMeta(type):
104
105
  cls.name = attrs.get("name", prettify_class_name(cls.model.__name__))
105
106
  cls.name_plural = attrs.get("name_plural", f"{cls.name}s")
106
107
 
107
- mcls._check_conflicting_options(["column_list", "column_exclude_list"], attrs)
108
- mcls._check_conflicting_options(
109
- ["form_columns", "form_excluded_columns"], attrs
110
- )
111
- mcls._check_conflicting_options(
108
+ mcs._check_conflicting_options(["column_list", "column_exclude_list"], attrs)
109
+ mcs._check_conflicting_options(["form_columns", "form_excluded_columns"], attrs)
110
+ mcs._check_conflicting_options(
112
111
  ["column_details_list", "column_details_exclude_list"], attrs
113
112
  )
114
- mcls._check_conflicting_options(
113
+ mcs._check_conflicting_options(
115
114
  ["column_export_list", "column_export_exclude_list"], attrs
116
115
  )
117
116
 
118
117
  return cls
119
118
 
120
119
  @classmethod
121
- def _check_conflicting_options(mcls, keys: List[str], attrs: dict) -> None:
120
+ def _check_conflicting_options(mcs, keys: List[str], attrs: dict) -> None:
122
121
  if all(k in attrs for k in keys):
123
122
  raise AssertionError(f"Cannot use {' and '.join(keys)} together.")
124
123
 
@@ -211,7 +210,12 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
211
210
 
212
211
  # Internals
213
212
  pk_columns: ClassVar[Tuple[Column]]
214
- session_maker: ClassVar[Union[sessionmaker, "async_sessionmaker"]]
213
+ session_maker: ClassVar[ # type: ignore[no-any-unimported]
214
+ Union[
215
+ sessionmaker,
216
+ "async_sessionmaker",
217
+ ]
218
+ ]
215
219
  is_async: ClassVar[bool] = False
216
220
  is_model: ClassVar[bool] = True
217
221
  ajax_lookup_url: ClassVar[str] = ""
@@ -491,6 +495,17 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
491
495
  Unlimited by default.
492
496
  """
493
497
 
498
+ use_pretty_export: ClassVar[bool] = False
499
+ """
500
+ Enable export of CSV files using column labels and column formatters.
501
+
502
+ If set to True, the export will apply column labels and formatting logic
503
+ used in the list template.
504
+ Otherwise, raw database values and field names will be used.
505
+
506
+ You can override cell formatting per column by implementing `custom_export_cell`.
507
+ """
508
+
494
509
  # Form
495
510
  form: ClassVar[Optional[Type[Form]]] = None
496
511
  """Form class.
@@ -803,8 +818,8 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
803
818
  return self.column_default_sort
804
819
  if isinstance(self.column_default_sort, tuple):
805
820
  return [self.column_default_sort]
806
- else:
807
- return [(self.column_default_sort, False)]
821
+
822
+ return [(self.column_default_sort, False)]
808
823
 
809
824
  return [(pk.name, False) for pk in self.pk_columns]
810
825
 
@@ -821,10 +836,10 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
821
836
 
822
837
  try:
823
838
  return int(number)
824
- except ValueError:
839
+ except ValueError as exc:
825
840
  raise HTTPException(
826
841
  status_code=400, detail="Invalid page or pageSize parameter"
827
- )
842
+ ) from exc
828
843
 
829
844
  async def count(self, request: Request, stmt: Optional[Select] = None) -> int:
830
845
  if stmt is None:
@@ -842,14 +857,14 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
842
857
  for relation in self._list_relations:
843
858
  stmt = stmt.options(selectinload(relation))
844
859
 
845
- for filter in self.get_filters():
846
- filter_param_name = filter.parameter_name
860
+ for filter_ in self.get_filters():
861
+ filter_param_name = filter_.parameter_name
847
862
  filter_value = request.query_params.get(filter_param_name)
848
863
 
849
864
  if filter_value:
850
- if hasattr(filter, "has_operator") and filter.has_operator:
865
+ if hasattr(filter_, "has_operator") and filter_.has_operator:
851
866
  # Use operation-based filtering
852
- operation_filter = typing_cast(OperationColumnFilter, filter)
867
+ operation_filter = typing_cast(OperationColumnFilter, filter_)
853
868
  operation_param = request.query_params.get(
854
869
  f"{filter_param_name}_op"
855
870
  )
@@ -859,7 +874,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
859
874
  )
860
875
  else:
861
876
  # Use simple filtering for filters without operators
862
- simple_filter = typing_cast(SimpleColumnFilter, filter)
877
+ simple_filter = typing_cast(SimpleColumnFilter, filter_)
863
878
  stmt = await simple_filter.get_filtered_query(
864
879
  stmt, filter_value, self.model
865
880
  )
@@ -869,7 +884,9 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
869
884
  if search:
870
885
  stmt = self.search_query(stmt=stmt, term=search)
871
886
 
872
- count = await self.count(request, select(func.count()).select_from(stmt))
887
+ count = await self.count(
888
+ request, select(func.count()).select_from(stmt.subquery())
889
+ )
873
890
 
874
891
  stmt = stmt.limit(page_size).offset((page - 1) * page_size)
875
892
  rows = await self._run_query(stmt)
@@ -971,14 +988,16 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
971
988
  """This function generalizes constructing a list of columns
972
989
  for any sequence of inclusions or exclusions.
973
990
  """
974
-
975
991
  if include == "__all__":
976
992
  return self._prop_names
977
- elif include:
993
+
994
+ if include:
978
995
  return [self._get_prop_name(item) for item in include]
979
- elif exclude:
996
+
997
+ if exclude:
980
998
  exclude = [self._get_prop_name(item) for item in exclude]
981
999
  return [prop for prop in self._prop_names if prop not in exclude]
1000
+
982
1001
  return defaults
983
1002
 
984
1003
  def get_list_columns(self) -> List[str]:
@@ -1087,7 +1106,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1087
1106
 
1088
1107
  form = await get_model_form(
1089
1108
  model=self.model,
1090
- session_maker=self.session_maker,
1109
+ session_maker=self.session_maker, # type: ignore[arg-type]
1091
1110
  only=self._form_prop_names,
1092
1111
  column_labels=self._column_labels,
1093
1112
  form_args=self.form_args,
@@ -1231,9 +1250,16 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1231
1250
  export_type: str = "csv",
1232
1251
  ) -> StreamingResponse:
1233
1252
  if export_type == "csv":
1234
- return await self._export_csv(data)
1235
- elif export_type == "json":
1253
+ export_method = (
1254
+ PrettyExport.pretty_export_csv(self, data)
1255
+ if self.use_pretty_export
1256
+ else self._export_csv(data)
1257
+ )
1258
+ return await export_method
1259
+
1260
+ if export_type == "json":
1236
1261
  return await self._export_json(data)
1262
+
1237
1263
  raise NotImplementedError("Only export_type='csv' or 'json' is implemented.")
1238
1264
 
1239
1265
  async def _export_csv(
@@ -1289,6 +1315,22 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1289
1315
  headers={"Content-Disposition": f"attachment;filename={filename}"},
1290
1316
  )
1291
1317
 
1318
+ async def custom_export_cell(
1319
+ self,
1320
+ row: Any,
1321
+ name: str,
1322
+ value: Any,
1323
+ ) -> Optional[str]:
1324
+ """
1325
+ Override to provide custom formatting for a specific cell in pretty export.
1326
+
1327
+ Return a string to override the default formatting for the given field,
1328
+ or return None to fall back to `base_export_cell`.
1329
+
1330
+ Only used when `use_pretty_export = True`.
1331
+ """
1332
+ return None
1333
+
1292
1334
  def _refresh_form_rules_cache(self) -> None:
1293
1335
  if self.form_rules:
1294
1336
  self._form_create_rules = self.form_rules
@@ -0,0 +1,75 @@
1
+ from typing import TYPE_CHECKING, Any, AsyncGenerator, List
2
+
3
+ from starlette.responses import StreamingResponse
4
+
5
+ from sqladmin.helpers import Writer, secure_filename, stream_to_csv
6
+
7
+ if TYPE_CHECKING:
8
+ from .models import ModelView
9
+
10
+
11
+ class PrettyExport:
12
+ @staticmethod
13
+ async def _base_export_cell(
14
+ model_view: "ModelView", name: str, value: Any, formatted_value: Any
15
+ ) -> str:
16
+ """
17
+ Default formatting logic for a cell in pretty export.
18
+
19
+ Used when `custom_export_cell` returns None.
20
+ Applies standard rules for related fields, booleans, etc.
21
+
22
+ Only used when `use_pretty_export = True`.
23
+ """
24
+ if name in model_view._relation_names:
25
+ if isinstance(value, list):
26
+ cell_value = ",".join(formatted_value)
27
+ else:
28
+ cell_value = formatted_value
29
+ else:
30
+ if isinstance(value, bool):
31
+ cell_value = "TRUE" if value else "FALSE"
32
+ else:
33
+ cell_value = formatted_value
34
+ return cell_value
35
+
36
+ @classmethod
37
+ async def _get_export_row_values(
38
+ cls, model_view: "ModelView", row: Any, column_names: List[str]
39
+ ) -> List[Any]:
40
+ row_values = []
41
+ for name in column_names:
42
+ value, formatted_value = await model_view.get_list_value(row, name)
43
+ custom_value = await model_view.custom_export_cell(row, name, value)
44
+ if custom_value is None:
45
+ cell_value = await cls._base_export_cell(
46
+ model_view, name, value, formatted_value
47
+ )
48
+ else:
49
+ cell_value = custom_value
50
+ row_values.append(cell_value)
51
+ return row_values
52
+
53
+ @classmethod
54
+ async def pretty_export_csv(
55
+ cls, model_view: "ModelView", rows: List[Any]
56
+ ) -> StreamingResponse:
57
+ async def generate(writer: Writer) -> AsyncGenerator[Any, None]:
58
+ column_names = model_view.get_export_columns()
59
+ headers = [
60
+ model_view._column_labels.get(name, name) for name in column_names
61
+ ]
62
+
63
+ yield writer.writerow(headers)
64
+
65
+ for row in rows:
66
+ vals = await cls._get_export_row_values(model_view, row, column_names)
67
+ yield writer.writerow(vals)
68
+
69
+ filename = secure_filename(model_view.get_export_name(export_type="csv"))
70
+
71
+ return StreamingResponse(
72
+ content=stream_to_csv(generate),
73
+ media_type="text/csv",
74
+ headers={"Content-Disposition": f"attachment;filename={filename}"},
75
+ )
@@ -5,7 +5,7 @@
5
5
  <div class="card-header">
6
6
  <h3 class="card-title">
7
7
  {% for pk in model_view.pk_columns -%}
8
- {{ pk.name }}
8
+ {{ pk.name | title }}
9
9
  {%- if not loop.last %};{% endif -%}
10
10
  {% endfor %}: {{ get_object_identifier(model) }}</h3>
11
11
  </div>
@@ -48,14 +48,14 @@
48
48
  </table>
49
49
  </div>
50
50
  <div class="card-footer container">
51
- <div class="row">
52
- <div class="col-md-1">
51
+ <div class="row row-gap-2">
52
+ <div class="col-auto">
53
53
  <a href="{{ url_for('admin:list', identity=model_view.identity) }}" class="btn">
54
54
  Go Back
55
55
  </a>
56
56
  </div>
57
57
  {% if model_view.can_delete %}
58
- <div class="col-md-1">
58
+ <div class="col-auto">
59
59
  <a href="#" data-name="{{ model_view.name }}" data-pk="{{ get_object_identifier(model) }}"
60
60
  data-url="{{ model_view._url_for_delete(request, model) }}" data-bs-toggle="modal"
61
61
  data-bs-target="#modal-delete" class="btn btn-danger">
@@ -64,14 +64,14 @@
64
64
  </div>
65
65
  {% endif %}
66
66
  {% if model_view.can_edit %}
67
- <div class="col-md-1">
67
+ <div class="col-auto">
68
68
  <a href="{{ model_view._build_url_for('admin:edit', request, model) }}" class="btn btn-primary">
69
69
  Edit
70
70
  </a>
71
71
  </div>
72
72
  {% endif %}
73
73
  {% for custom_action,label in model_view._custom_actions_in_detail.items() %}
74
- <div class="col-md-1">
74
+ <div class="col-auto">
75
75
  {% if custom_action in model_view._custom_actions_confirmation %}
76
76
  <a href="#" class="btn btn-secondary" data-bs-toggle="modal"
77
77
  data-bs-target="#modal-confirmation-{{ custom_action }}">
@@ -103,4 +103,4 @@ url=model_view._url_for_action(request, custom_action) + '?pks=' + (get_object_i
103
103
  {% endif %}
104
104
  {% endfor %}
105
105
 
106
- {% endblock %}
106
+ {% endblock %}
@@ -265,12 +265,23 @@
265
265
  {% else %}
266
266
  <!-- Fallback for other filter types -->
267
267
  <div class="mb-3">
268
- <div class="fw-bold text-truncate">{{ filter.title }}</div>
268
+ <div class="fw-bold text-truncate fs-3 mb-2">{{ filter.title }}</div>
269
269
  <div>
270
270
  {% for lookup in filter.lookups(request, model_view.model, model_view._run_arbitrary_query) %}
271
- <a href="{{ request.url.include_query_params(**{filter.parameter_name: lookup[0]}) }}" class="d-block text-decoration-none text-truncate">
271
+ {% if request.query_params.get(filter.parameter_name) == lookup[0] %}
272
+ <div class="d-flex align-items-center justify-content-between bg-secondary-lt px-2 py-1 rounded">
273
+ <span class="text-truncate fw-bold text-dark">
274
+ {{ lookup[1] }}
275
+ </span>
276
+ <a href="{{ request.url.remove_query_params(filter.parameter_name) }}" class="text-decoration-none ms-2" title="Clear filter">
277
+ <i class="fa-solid fa-times"></i>
278
+ </a>
279
+ </div>
280
+ {% else %}
281
+ <a href="{{ request.url.include_query_params(**{filter.parameter_name: lookup[0]}) }}" class="d-block text-decoration-none text-truncate px-2 py-1">
272
282
  {{ lookup[1] }}
273
283
  </a>
284
+ {% endif %}
274
285
  {% endfor %}
275
286
  </div>
276
287
  </div>
sqladmin/templating.py CHANGED
@@ -45,7 +45,7 @@ class Jinja2Templates:
45
45
  def __init__(self, directory: str) -> None:
46
46
  @jinja2.pass_context
47
47
  def url_for(context: dict, __name: str, **path_params: Any) -> URL:
48
- request = context["request"]
48
+ request: Request = context["request"]
49
49
  return request.url_for(__name, **path_params)
50
50
 
51
51
  loader = jinja2.FileSystemLoader(directory)
sqladmin/widgets.py CHANGED
@@ -1,10 +1,15 @@
1
+ # mypy: disable-error-code="override"
2
+
1
3
  import json
2
- from typing import Any
4
+ from typing import TYPE_CHECKING, Any
3
5
 
4
6
  from markupsafe import Markup
5
- from wtforms import Field, widgets
7
+ from wtforms import Field, SelectFieldBase, widgets
6
8
  from wtforms.widgets import html_params
7
9
 
10
+ if TYPE_CHECKING:
11
+ from sqladmin.fields import AjaxSelectField
12
+
8
13
  __all__ = [
9
14
  "AjaxSelect2Widget",
10
15
  "DatePickerWidget",
@@ -38,7 +43,7 @@ class AjaxSelect2Widget(widgets.Select):
38
43
  self.multiple = multiple
39
44
  self.lookup_url = ""
40
45
 
41
- def __call__(self, field: Field, **kwargs: Any) -> Markup:
46
+ def __call__(self, field: "AjaxSelectField", **kwargs: Any) -> Markup:
42
47
  kwargs.setdefault("data-role", "select2-ajax")
43
48
  kwargs.setdefault("data-url", field.loader.model_admin.ajax_lookup_url)
44
49
 
@@ -61,11 +66,11 @@ class AjaxSelect2Widget(widgets.Select):
61
66
  if data:
62
67
  kwargs["data-json"] = json.dumps([data])
63
68
 
64
- return Markup(f"<select {html_params(name=field.name, **kwargs)}></select>")
69
+ return Markup(f"<select {html_params(name=field.name, **kwargs)}></select>") # nosec: markupsafe_markup_xss
65
70
 
66
71
 
67
72
  class Select2TagsWidget(widgets.Select):
68
- def __call__(self, field: Field, **kwargs: Any) -> str:
73
+ def __call__(self, field: SelectFieldBase, **kwargs: Any) -> str:
69
74
  kwargs.setdefault("data-role", "select2-tags")
70
75
  kwargs.setdefault("data-json", json.dumps(field.data))
71
76
  kwargs.setdefault("multiple", "multiple")
@@ -81,20 +86,21 @@ class FileInputWidget(widgets.FileInput):
81
86
  if not field.flags.required:
82
87
  checkbox_id = f"{field.id}_checkbox"
83
88
  checkbox_label = Markup(
84
- f'<label class="form-check-label" for="{checkbox_id}">Clear</label>'
85
- )
89
+ '<label class="form-check-label" for="{}">Clear</label>'
90
+ ).format(checkbox_id)
91
+
86
92
  checkbox_input = Markup(
87
- f'<input class="form-check-input" type="checkbox" id="{checkbox_id}" name="{checkbox_id}">' # noqa: E501
88
- )
89
- checkbox = Markup(
90
- f'<div class="form-check">{checkbox_input}{checkbox_label}</div>'
93
+ '<input class="form-check-input" type="checkbox" id="{}" name="{}">' # noqa: E501
94
+ ).format(checkbox_id, checkbox_id)
95
+ checkbox = Markup('<div class="form-check">{}{}</div>').format(
96
+ checkbox_input, checkbox_label
91
97
  )
92
98
  else:
93
99
  checkbox = Markup()
94
100
 
95
101
  if field.data:
96
- current_value = Markup(f"<p>Currently: {field.data}</p>")
102
+ current_value = Markup("<p>Currently: {}</p>").format(field.data)
97
103
  field.flags.required = False
98
104
  return current_value + checkbox + super().__call__(field, **kwargs)
99
- else:
100
- return super().__call__(field, **kwargs)
105
+
106
+ return super().__call__(field, **kwargs)
@@ -1,19 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqladmin
3
- Version: 0.22.0
3
+ Version: 0.23.0
4
4
  Summary: SQLAlchemy admin for FastAPI and Starlette
5
- Project-URL: Documentation, https://aminalaee.github.io/sqladmin/
6
- Project-URL: Issues, https://github.com/aminalaee/sqladmin/issues
7
- Project-URL: Source, https://github.com/aminalaee/sqladmin
5
+ Keywords: sqlalchemy,fastapi,starlette,admin
6
+ Author: Amin Alaee
8
7
  Author-email: Amin Alaee <me@aminalaee.dev>
9
8
  License-Expression: BSD-3-Clause
10
- License-File: LICENSE.md
11
- Keywords: admin,fastapi,sqlalchemy,starlette
12
9
  Classifier: Development Status :: 4 - Beta
13
- Classifier: Environment :: Web Environment
14
- Classifier: Intended Audience :: Developers
15
- Classifier: License :: OSI Approved :: BSD License
16
- Classifier: Operating System :: OS Independent
17
10
  Classifier: Programming Language :: Python
18
11
  Classifier: Programming Language :: Python :: 3.9
19
12
  Classifier: Programming Language :: Python :: 3.10
@@ -21,15 +14,22 @@ Classifier: Programming Language :: Python :: 3.11
21
14
  Classifier: Programming Language :: Python :: 3.12
22
15
  Classifier: Programming Language :: Python :: 3.13
23
16
  Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Environment :: Web Environment
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: License :: OSI Approved :: BSD License
24
20
  Classifier: Topic :: Internet :: WWW/HTTP
25
- Requires-Python: >=3.10
21
+ Classifier: Operating System :: OS Independent
22
+ Requires-Dist: starlette
23
+ Requires-Dist: wtforms>=3.1,<3.2
26
24
  Requires-Dist: jinja2
27
25
  Requires-Dist: python-multipart
28
- Requires-Dist: sqlalchemy>=1.4
29
- Requires-Dist: starlette
30
- Requires-Dist: wtforms<3.2,>=3.1
26
+ Requires-Dist: sqlalchemy>=2.0
27
+ Requires-Dist: itsdangerous ; extra == 'full'
28
+ Requires-Python: >=3.9
29
+ Project-URL: Documentation, https://aminalaee.github.io/sqladmin/
30
+ Project-URL: Issues, https://github.com/aminalaee/sqladmin/issues
31
+ Project-URL: Source, https://github.com/aminalaee/sqladmin
31
32
  Provides-Extra: full
32
- Requires-Dist: itsdangerous; extra == 'full'
33
33
  Description-Content-Type: text/markdown
34
34
 
35
35
  <p align="center">
@@ -1,22 +1,21 @@
1
- sqladmin/__init__.py,sha256=HgCrQImuNIFeVjI_W2hpvmsR-I03N5VJjESatR_jeW0,216
2
- sqladmin/_menu.py,sha256=kjBIk_PIpa__C-gSRPIWv9OEQR1uFB4M_1LOU5MHYu0,2608
3
- sqladmin/_queries.py,sha256=KqxMABvepoA0j-8Xizg6ASYS-sZDSdm5iFlU74vilQY,9697
4
- sqladmin/_types.py,sha256=gFZIAM1rBDy3nVRPlxoWCS-gegtf6fSHkc0q35vmzAA,1542
5
- sqladmin/_validators.py,sha256=w0siGhZQq4MD__lu9Edua9DgMOoKET_kk-alpARFHIM,1604
6
- sqladmin/ajax.py,sha256=wSP5P9cAIh3GImIc_-E_Mi14aJAcbtiy4_pDPukTs5E,2764
7
- sqladmin/application.py,sha256=4uRwZNbzGjVnlphU1MwRuKBSink5UXbtR1tlkD9XmyU,27827
1
+ sqladmin/__init__.py,sha256=x3o2l-V1Oqfo_M7VHWGmtU8JoHYLWBwo7NoTbzBBXwQ,192
2
+ sqladmin/_menu.py,sha256=fDDh1g-t2Ww-n4Zipjw3UmMRRnxK-V9AaBpZOgpbbMs,2607
3
+ sqladmin/_queries.py,sha256=F-CZOtVfbHmynWNp3DEyfbux5EYbk4cCnj35zAYgkqI,9853
4
+ sqladmin/_types.py,sha256=6lF7cuzMaG5ZzbLIxA4-y1nsnax5Kyi-_Tc7oR4UmZA,1524
5
+ sqladmin/_validators.py,sha256=753oc74e9BQWBCZD9sZ-akHoqwjR76oHLRIobtCk99E,1758
6
+ sqladmin/ajax.py,sha256=vgpR6OWYItHPyW21cBi6qaCCgbIHprXhzXdt1OjR9oI,2780
7
+ sqladmin/application.py,sha256=PgShcWqDylJRZT1VIZ63_6t7-hjMvilDPwf5S3szJI0,28394
8
8
  sqladmin/authentication.py,sha256=VLNa38rzvQ774c_I1duI5dUVw3-LdcGFyIPafHgXtxg,2665
9
9
  sqladmin/exceptions.py,sha256=6-E8m7rbWE3A7hNaSmB6CVqFzkEuwUpmU5AdGbouPCw,154
10
- sqladmin/fields.py,sha256=1CWoVSMr1WkhBJww0-rakx71gRATeIGA6dKgc26z99M,11660
11
- sqladmin/filters.py,sha256=TJcSR_RnYov6ZeyYYQPHl8oeiJTDmbsebrkmRt9nz5c,11417
12
- sqladmin/formatters.py,sha256=K06la0mm9-Bs5UA9L6KGJC_X_lV3UHdJ3ENI6j9j2Zg,480
13
- sqladmin/forms.py,sha256=5VhbRWbsG23eDAGz2c03HnED-titNkBdYzDr-TaBSi0,21541
14
- sqladmin/helpers.py,sha256=nSKduAiWhUUKC-eGlURXQwQQzlqAh8X4Bl1AVxoY-DM,8908
15
- sqladmin/models.py,sha256=dbF80UJYeVNz4s1yyMH_N72PM84zuRjW5WqCfcXYLbE,43529
10
+ sqladmin/fields.py,sha256=lDnAAsxGOVZ_7ZYdy5Eox8PjntBy9EdnLfPSEgIWKKA,12083
11
+ sqladmin/filters.py,sha256=oplxJkM8XlWsGlyLBkyCki8z3uOBk9WAcRJCKN4Nx7U,11465
12
+ sqladmin/formatters.py,sha256=ywcQU5neKdFSTFdUTpQXSmpDm4ijVl2zSfOUwdaSPhw,488
13
+ sqladmin/forms.py,sha256=2Z1CAKxvFrOnFjyWJsqlkejfh_ncEqI7JNSap-MSi3w,22470
14
+ sqladmin/helpers.py,sha256=FKVIV3SxXfyBP5FCSdnwSGveIsnlKVDpFYSB2ZkRf9A,9029
15
+ sqladmin/models.py,sha256=4cPSgmgtRv1yF_kKlPZSX4kA4udQ6bydet7CPK3sG2o,44765
16
16
  sqladmin/pagination.py,sha256=zg_bAvqZd2Rf0wKJ7uiVfNV9vR0hrsilmi9Ak0SOG_U,2600
17
+ sqladmin/pretty_export.py,sha256=83zASMWLsY0a3g_Orj17_1hSK6XRqWorRBsnu1uPxmE,2609
17
18
  sqladmin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- sqladmin/templating.py,sha256=o-QMikTrOEgrneLonqCWR3SpAthr-9DoMwOmobM9zq0,2252
19
- sqladmin/widgets.py,sha256=xl9tGhj--KTRPmNhFn3WVbvN_tQfkYSNVBGicsFhrgM,3189
20
19
  sqladmin/statics/css/flatpickr.min.css,sha256=GzSkJVLJbxDk36qko2cnawOGiqz_Y8GsQv_jMTUrx1Q,16166
21
20
  sqladmin/statics/css/fontawesome.min.css,sha256=CTSx_A06dm1B063156EVh15m6Y67pAjZZaQc89LLSrU,102217
22
21
  sqladmin/statics/css/main.css,sha256=BeLxW6X9i_hoGHiXR-AQsdTRi7QPjBG1S2XUo0iFnS4,133
@@ -38,17 +37,18 @@ sqladmin/statics/webfonts/tabler-icons.woff2,sha256=26WaCnr1NnLG_uLQ3DMhEkenPiJJ
38
37
  sqladmin/templates/sqladmin/_macros.html,sha256=79GzkgJAaRbzhjm5WIRFkgl8W0U_5hH4gLteQan1rpo,2983
39
38
  sqladmin/templates/sqladmin/base.html,sha256=u6rdmdI6Kg7JteTFmLwh7UhIo2Z1yvZwCNvPnLrHAsg,1783
40
39
  sqladmin/templates/sqladmin/create.html,sha256=Vaj_OHLDIqnZF1HOz_g3ogTaCGZqPEfTJhaxezD1wjM,1396
41
- sqladmin/templates/sqladmin/details.html,sha256=CiH4qAFRl5PaHRWHLwCwABHvk5ghbkTPj51fEt_JQ28,4176
40
+ sqladmin/templates/sqladmin/details.html,sha256=kh1idWR7Usi6BvQjuhoWoEN8kypW8xRiit_trJGZMsE,4195
42
41
  sqladmin/templates/sqladmin/edit.html,sha256=geKD5j8ZLcMSciI80-qg_dQM3Sn_7g3_DeIONmSzQt8,1641
43
42
  sqladmin/templates/sqladmin/error.html,sha256=gb-172SMuQKncv0QE8DQdQXeM-fw7oXC0LPLO3ia0IM,290
44
43
  sqladmin/templates/sqladmin/index.html,sha256=vh_IhhYmHPOkdZNrXSEc4e9gXXeZ-nsRBCsJQ_mC7YI,71
45
44
  sqladmin/templates/sqladmin/layout.html,sha256=iBIhypkXp6O3hAHDdMNc4pWd9yxt5mQy7o2lBQD-6Ec,1994
46
- sqladmin/templates/sqladmin/list.html,sha256=uo5Ns5xx10Ze8hQ-QblArc2hMezDZS_1u0AtBG_BZmk,15912
45
+ sqladmin/templates/sqladmin/list.html,sha256=U1O5uG8lLUKVDVWe68nErv_tYsyBouz5I8lNIsMM_AA,16581
47
46
  sqladmin/templates/sqladmin/login.html,sha256=Y_hlcIapfVFPNbSIbCe4Tbj5DLLD46emkSlL5-RP4iY,1514
48
47
  sqladmin/templates/sqladmin/modals/delete.html,sha256=jTuv6geT-AhK5HTgRmntrJ8CEi98-kwKrVDrzkOQWhw,1092
49
48
  sqladmin/templates/sqladmin/modals/details_action_confirmation.html,sha256=mN8LJ5OqypxNLAg2_GYZgQmGeK4E6t7JL5RmOEYuliM,1020
50
49
  sqladmin/templates/sqladmin/modals/list_action_confirmation.html,sha256=U52LLNmpLaMuUZSVtGK15oLXsEu6m2S3l9zj9sjN6uM,1078
51
- sqladmin-0.22.0.dist-info/METADATA,sha256=-By02GmFg6euFsOK7mixrIxujx9sckUQDkt_IYtKJT4,5342
52
- sqladmin-0.22.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
- sqladmin-0.22.0.dist-info/licenses/LICENSE.md,sha256=4zzpHQMPtND4hzIgJA5qnb4R_wRBWJlYGqNrZolBeP8,1488
54
- sqladmin-0.22.0.dist-info/RECORD,,
50
+ sqladmin/templating.py,sha256=GcvfK54U_fypeDMySOWSg32dOCvAp8WfZUFMZqOXb9M,2261
51
+ sqladmin/widgets.py,sha256=YH7QquQkHviviu3GLTF0GG765MCXyk17SP1sXKC_LM0,3401
52
+ sqladmin-0.23.0.dist-info/WHEEL,sha256=5DEXXimM34_d4Gx1AuF9ysMr1_maoEtGKjaILM3s4w4,80
53
+ sqladmin-0.23.0.dist-info/METADATA,sha256=HsBgmbvjKmFLPDCnM5iL0-2qXxFV9-KR6ZK5HkJzRCc,5336
54
+ sqladmin-0.23.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: uv 0.9.29
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,27 +0,0 @@
1
- Copyright © 2022, Amin Alaee.
2
- All rights reserved.
3
-
4
- Redistribution and use in source and binary forms, with or without
5
- modification, are permitted provided that the following conditions are met:
6
-
7
- * Redistributions of source code must retain the above copyright notice, this
8
- list of conditions and the following disclaimer.
9
-
10
- * Redistributions in binary form must reproduce the above copyright notice,
11
- this list of conditions and the following disclaimer in the documentation
12
- and/or other materials provided with the distribution.
13
-
14
- * Neither the name of the copyright holder nor the names of its
15
- contributors may be used to endorse or promote products derived from
16
- this software without specific prior written permission.
17
-
18
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.