dflango 0.3.2__tar.gz → 0.3.4__tar.gz

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 (67) hide show
  1. {dflango-0.3.2/dflango.egg-info → dflango-0.3.4}/PKG-INFO +1 -1
  2. {dflango-0.3.2 → dflango-0.3.4}/dflango/__init__.py +1 -1
  3. {dflango-0.3.2 → dflango-0.3.4}/dflango/exports/csv/schemas.py +37 -1
  4. {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/base.py +4 -3
  5. {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/registry.py +29 -1
  6. {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/schema_factory.py +67 -2
  7. {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/views.py +43 -0
  8. {dflango-0.3.2 → dflango-0.3.4/dflango.egg-info}/PKG-INFO +1 -1
  9. {dflango-0.3.2 → dflango-0.3.4}/pyproject.toml +1 -1
  10. {dflango-0.3.2 → dflango-0.3.4}/LICENSE +0 -0
  11. {dflango-0.3.2 → dflango-0.3.4}/MANIFEST.in +0 -0
  12. {dflango-0.3.2 → dflango-0.3.4}/README.md +0 -0
  13. {dflango-0.3.2 → dflango-0.3.4}/dflango/commands/__init__.py +0 -0
  14. {dflango-0.3.2 → dflango-0.3.4}/dflango/commands/command_registration.py +0 -0
  15. {dflango-0.3.2 → dflango-0.3.4}/dflango/commands/load_fixtures.py +0 -0
  16. {dflango-0.3.2 → dflango-0.3.4}/dflango/commands/start_app.py +0 -0
  17. {dflango-0.3.2 → dflango-0.3.4}/dflango/config.py +0 -0
  18. {dflango-0.3.2 → dflango-0.3.4}/dflango/core.py +0 -0
  19. {dflango-0.3.2 → dflango-0.3.4}/dflango/db.py +0 -0
  20. {dflango-0.3.2 → dflango-0.3.4}/dflango/encoders.py +0 -0
  21. {dflango-0.3.2 → dflango-0.3.4}/dflango/enum.py +0 -0
  22. {dflango-0.3.2 → dflango-0.3.4}/dflango/exports/__init__.py +0 -0
  23. {dflango-0.3.2 → dflango-0.3.4}/dflango/exports/csv/__init__.py +0 -0
  24. {dflango-0.3.2 → dflango-0.3.4}/dflango/exports/csv/fields.py +0 -0
  25. {dflango-0.3.2 → dflango-0.3.4}/dflango/exports/csv/registry.py +0 -0
  26. {dflango-0.3.2 → dflango-0.3.4}/dflango/logging.py +0 -0
  27. {dflango-0.3.2 → dflango-0.3.4}/dflango/mixin/__init__.py +0 -0
  28. {dflango-0.3.2 → dflango-0.3.4}/dflango/mixin/permission_mixin.py +0 -0
  29. {dflango-0.3.2 → dflango-0.3.4}/dflango/permissions.py +0 -0
  30. {dflango-0.3.2 → dflango-0.3.4}/dflango/providers.py +0 -0
  31. {dflango-0.3.2 → dflango-0.3.4}/dflango/routes/__init__.py +0 -0
  32. {dflango-0.3.2 → dflango-0.3.4}/dflango/routes/route_registry.py +0 -0
  33. {dflango-0.3.2 → dflango-0.3.4}/dflango/schemas/__init__.py +0 -0
  34. {dflango-0.3.2 → dflango-0.3.4}/dflango/schemas/fields.py +0 -0
  35. {dflango-0.3.2 → dflango-0.3.4}/dflango/schemas/model_schemas.py +0 -0
  36. {dflango-0.3.2 → dflango-0.3.4}/dflango/schemas/schemas.py +0 -0
  37. {dflango-0.3.2 → dflango-0.3.4}/dflango/services/__init__.py +0 -0
  38. {dflango-0.3.2 → dflango-0.3.4}/dflango/services/auth.py +0 -0
  39. {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/__init__.py +0 -0
  40. {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/commands.py +0 -0
  41. {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/exceptions.py +0 -0
  42. {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/__init__.py +0 -0
  43. {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/config.py +0 -0
  44. {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/constants.py +0 -0
  45. {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/models/__init__.py +0 -0
  46. {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/models/models.py +0 -0
  47. {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/schemas/__init__.py +0 -0
  48. {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/schemas/schemas.py +0 -0
  49. {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/urls.py +0 -0
  50. {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/views/__init__.py +0 -0
  51. {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/views/views.py +0 -0
  52. {dflango-0.3.2 → dflango-0.3.4}/dflango/views/__init__.py +0 -0
  53. {dflango-0.3.2 → dflango-0.3.4}/dflango/views/base_export_view.py +0 -0
  54. {dflango-0.3.2 → dflango-0.3.4}/dflango/views/base_view.py +0 -0
  55. {dflango-0.3.2 → dflango-0.3.4}/dflango/views/config_view.py +0 -0
  56. {dflango-0.3.2 → dflango-0.3.4}/dflango/views/detail_view.py +0 -0
  57. {dflango-0.3.2 → dflango-0.3.4}/dflango/views/general_list_view.py +0 -0
  58. {dflango-0.3.2 → dflango-0.3.4}/dflango/views/internal_view.py +0 -0
  59. {dflango-0.3.2 → dflango-0.3.4}/dflango/views/list_view.py +0 -0
  60. {dflango-0.3.2 → dflango-0.3.4}/dflango/views/search_view.py +0 -0
  61. {dflango-0.3.2 → dflango-0.3.4}/dflango/views/statistic_view.py +0 -0
  62. {dflango-0.3.2 → dflango-0.3.4}/dflango.egg-info/SOURCES.txt +0 -0
  63. {dflango-0.3.2 → dflango-0.3.4}/dflango.egg-info/dependency_links.txt +0 -0
  64. {dflango-0.3.2 → dflango-0.3.4}/dflango.egg-info/requires.txt +0 -0
  65. {dflango-0.3.2 → dflango-0.3.4}/dflango.egg-info/top_level.txt +0 -0
  66. {dflango-0.3.2 → dflango-0.3.4}/setup.cfg +0 -0
  67. {dflango-0.3.2 → dflango-0.3.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dflango
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Django-like utilities for Flask applications
5
5
  Author-email: Martino Scarcia <martino.scarcia@hybrissoftware.it>
6
6
  License-Expression: MIT
@@ -2,7 +2,7 @@
2
2
  dflango - Django-like utilities for Flask applications
3
3
  """
4
4
 
5
- __version__ = "0.3.2"
5
+ __version__ = "0.3.4"
6
6
 
7
7
  from dflango.core import DFlango, get_dflango, get_db
8
8
  from dflango.schemas import ModelSchema, UtcDateTimeField, EnumField
@@ -1,6 +1,9 @@
1
1
  # Built-in
2
2
  from datetime import datetime
3
3
 
4
+ # Third-party
5
+ from sqlalchemy import String
6
+
4
7
  # Local
5
8
  from dflango.exports.csv.fields import sanitize_csv_value
6
9
 
@@ -29,9 +32,42 @@ class CSVExportSchema:
29
32
  delimiter: str = ","
30
33
  encoding: str = "utf-8"
31
34
  include_bom: bool = False
35
+ allowed_filters: dict = {}
32
36
 
33
37
  def validate_filters(self, filters: dict) -> dict:
34
- return filters
38
+ if not self.allowed_filters:
39
+ return filters
40
+ validated = {}
41
+ for field, lookups in self.allowed_filters.items():
42
+ for lookup in lookups:
43
+ key = f"{field}__{lookup}"
44
+ if key in filters:
45
+ validated[key] = filters[key]
46
+ return validated
47
+
48
+ def apply_filters(self, query, model_cls, filters: dict):
49
+ for key, value in filters.items():
50
+ if "__" not in key:
51
+ continue
52
+ field_name, lookup = key.rsplit("__", 1)
53
+ column = getattr(model_cls, field_name, None)
54
+ if column is None:
55
+ continue
56
+ if lookup == "icontains":
57
+ query = query.filter(column.cast(String).ilike(f"%{value}%"))
58
+ elif lookup == "exact":
59
+ try:
60
+ if column.property.columns[0].type.__class__.__name__ == "ARRAY":
61
+ query = query.filter(column.any(value))
62
+ else:
63
+ query = query.filter(column == value)
64
+ except Exception:
65
+ query = query.filter(column == value)
66
+ elif lookup == "gte":
67
+ query = query.filter(column >= value)
68
+ elif lookup == "lte":
69
+ query = query.filter(column <= value)
70
+ return query
35
71
 
36
72
  def check_permissions(self, context: ExportContext) -> None:
37
73
  pass
@@ -1,6 +1,6 @@
1
1
  import uuid
2
2
 
3
- from sqlalchemy import Column, DateTime
3
+ from sqlalchemy import Boolean, Column, DateTime
4
4
  from sqlalchemy.dialects.postgresql import UUID
5
5
  from sqlalchemy.sql import func
6
6
 
@@ -11,14 +11,15 @@ class SuperModel:
11
11
  Provides built-in common fields and introspection utilities.
12
12
 
13
13
  Usage:
14
- class Source(SuperModel, db.Model):
15
- __tablename__ = "sources"
14
+ class Category(SuperModel, db.Model):
15
+ __tablename__ = "categories"
16
16
  name = db.Column(db.String(255), nullable=False)
17
17
  """
18
18
 
19
19
  __abstract__ = True
20
20
 
21
21
  id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
22
+ is_active = Column(Boolean, nullable=False, default=True, server_default="true")
22
23
  created_at = Column(DateTime, nullable=False, server_default=func.now())
23
24
  updated_at = Column(
24
25
  DateTime, nullable=False, server_default=func.now(), onupdate=func.now()
@@ -41,6 +41,8 @@ class SuperModelConfig:
41
41
  schema_class=None,
42
42
  viewset_class=None,
43
43
  filters=None,
44
+ value_field="id",
45
+ label_field="name",
44
46
  ):
45
47
  self.model_class = model_class
46
48
  self.slug = slug
@@ -50,6 +52,8 @@ class SuperModelConfig:
50
52
  self.permissions = permissions
51
53
  self.schema_class = schema_class
52
54
  self.viewset_class = viewset_class
55
+ self.value_field = value_field
56
+ self.label_field = label_field
53
57
  # If filters is True or not specified, auto-generate from columns.
54
58
  # If filters is a dict, use it directly. If False/empty, no filtering.
55
59
  if filters is True or filters is None:
@@ -87,6 +91,8 @@ class SuperModelRegistry:
87
91
  schema_class=None,
88
92
  viewset_class=None,
89
93
  filters=None,
94
+ value_field="id",
95
+ label_field="name",
90
96
  ):
91
97
  """Decorator to register a SuperModel model.
92
98
 
@@ -97,6 +103,8 @@ class SuperModelRegistry:
97
103
  None/True = auto-generate from columns.
98
104
  False = disable filtering.
99
105
  Dict = use as-is.
106
+ value_field: Field to use as 'value' in the minimal endpoint. Default: 'id'.
107
+ label_field: Field to use as 'label' in the minimal endpoint. Default: 'name'.
100
108
  """
101
109
 
102
110
  def decorator(model_class):
@@ -118,6 +126,8 @@ class SuperModelRegistry:
118
126
  schema_class=schema_class,
119
127
  viewset_class=viewset_class,
120
128
  filters=filters,
129
+ value_field=value_field,
130
+ label_field=label_field,
121
131
  )
122
132
 
123
133
  self._items[resolved_slug] = config
@@ -201,6 +211,7 @@ class SuperModelRegistry:
201
211
  "detail": f"GET {config.endpoint_prefix}/<id>",
202
212
  "update": f"PUT {config.endpoint_prefix}/<id>",
203
213
  "delete": f"DELETE {config.endpoint_prefix}/<id>",
214
+ "minimal": f"GET {config.endpoint_prefix}/@minimal",
204
215
  },
205
216
  "fields": fields,
206
217
  "readonly_fields": list(model_class.READONLY_FIELDS),
@@ -233,7 +244,11 @@ class SuperModelRegistry:
233
244
  ...
234
245
  ] + support_registry.get_urlpatterns()
235
246
  """
236
- from dflango.supermodel.views import create_list_create_view, create_detail_view
247
+ from dflango.supermodel.views import (
248
+ create_list_create_view,
249
+ create_detail_view,
250
+ create_minimal_view,
251
+ )
237
252
 
238
253
  urlpatterns = []
239
254
 
@@ -256,4 +271,17 @@ class SuperModelRegistry:
256
271
  detail_view.as_view(detail_view_name),
257
272
  ))
258
273
 
274
+ # Minimal view (value/label for dropdowns)
275
+ minimal_schema = SuperModelSchemaFactory.create_minimal_schema(
276
+ config.model_class,
277
+ value_field=config.value_field,
278
+ label_field=config.label_field,
279
+ )
280
+ minimal_view = create_minimal_view(config, minimal_schema)
281
+ minimal_view_name = f"{config.slug}_minimal"
282
+ urlpatterns.append((
283
+ f"{config.endpoint_prefix}/@minimal",
284
+ minimal_view.as_view(minimal_view_name),
285
+ ))
286
+
259
287
  return urlpatterns
@@ -74,13 +74,14 @@ class SuperModelSchemaFactory:
74
74
 
75
75
  for column in model_class.get_public_columns():
76
76
  # Skip built-in fields, we add them explicitly below
77
- if column.name in ("id", "created_at", "updated_at", "deleted_at"):
77
+ if column.name in ("id", "is_active", "created_at", "updated_at", "deleted_at"):
78
78
  continue
79
79
  schema_fields[column.name] = _resolve_marshmallow_field(column)
80
80
  schema_fields[column.name].dump_only = True
81
81
 
82
82
  # Add built-in fields explicitly
83
83
  schema_fields["id"] = ma_fields.String(dump_only=True)
84
+ schema_fields["is_active"] = ma_fields.Boolean(dump_only=True)
84
85
  schema_fields["created_at"] = ma_fields.DateTime(dump_only=True)
85
86
  schema_fields["updated_at"] = ma_fields.DateTime(dump_only=True)
86
87
  schema_fields["deleted_at"] = ma_fields.DateTime(dump_only=True, allow_none=True)
@@ -91,7 +92,8 @@ class SuperModelSchemaFactory:
91
92
  @classmethod
92
93
  def _create_write_schema(cls, model_class):
93
94
  """Schema for deserialization (POST/PUT body).
94
- Extends ModelSchema to support validate() + save()."""
95
+ Extends ModelSchema to support validate() + save().
96
+ When an instance is set (PUT), validates in partial mode."""
95
97
  from dflango.schemas import ModelSchema
96
98
 
97
99
  schema_fields = {}
@@ -105,4 +107,67 @@ class SuperModelSchemaFactory:
105
107
  meta = type("Meta", (), {"model": model_class})
106
108
  schema_fields["Meta"] = meta
107
109
 
110
+ def validate_with_partial(self, data, *args, **kwargs):
111
+ """Override validate to use partial=True when updating (instance is set)."""
112
+ from flask import abort, make_response, jsonify
113
+ from marshmallow import ValidationError
114
+ from dflango.core import get_db
115
+
116
+ partial = self.instance is not None
117
+
118
+ try:
119
+ loaded = self.load(data, partial=partial, unknown=EXCLUDE)
120
+
121
+ model_cls = self.Meta.model
122
+ errors = {}
123
+
124
+ for column in model_cls.__table__.columns:
125
+ if column.unique or column.primary_key:
126
+ column_name = column.name
127
+ if column_name in loaded:
128
+ value = loaded[column_name]
129
+ if value is None:
130
+ continue
131
+ db = get_db()
132
+ query = db.session.query(model_cls).filter(
133
+ getattr(model_cls, column_name) == value
134
+ )
135
+ if self.instance and hasattr(self.instance, "id"):
136
+ query = query.filter(model_cls.id != self.instance.id)
137
+ if query.first() is not None:
138
+ label = column_name.replace("_", " ").capitalize()
139
+ msg = f"{label} '{value}' is already in use."
140
+ errors[column_name] = [msg]
141
+
142
+ if errors:
143
+ abort(make_response(jsonify(errors), 422))
144
+
145
+ self.validated_data = loaded
146
+
147
+ except ValidationError as err:
148
+ abort(make_response(jsonify(err.messages), 422))
149
+
150
+ schema_fields["validate"] = validate_with_partial
151
+
108
152
  return type(schema_name, (ModelSchema,), schema_fields)
153
+
154
+ @classmethod
155
+ def create_minimal_schema(cls, model_class, value_field="id", label_field="name"):
156
+ """Generate a minimal schema that returns only value and label fields.
157
+ Used for dropdown/select endpoints."""
158
+
159
+ schema_fields = {
160
+ "value": ma_fields.String(dump_only=True),
161
+ "label": ma_fields.String(dump_only=True),
162
+ }
163
+
164
+ def dump_fn(self, obj, **kwargs):
165
+ return {
166
+ "value": str(getattr(obj, value_field, None) or ""),
167
+ "label": str(getattr(obj, label_field, None) or ""),
168
+ }
169
+
170
+ schema_fields["dump"] = dump_fn
171
+
172
+ schema_name = f"{model_class.__name__}MinimalSchema"
173
+ return type(schema_name, (_SuperModelReadSchema,), schema_fields)
@@ -1,5 +1,6 @@
1
1
  from dflango.views.list_view import ListView
2
2
  from dflango.views.detail_view import DetailView
3
+ from dflango.views.base_view import BaseView
3
4
 
4
5
  import uuid as _uuid
5
6
 
@@ -104,3 +105,45 @@ def create_detail_view(config, schema_class):
104
105
  view_class.get_instance = get_instance
105
106
 
106
107
  return view_class
108
+
109
+
110
+ def create_minimal_view(config, minimal_schema):
111
+ """
112
+ Dynamically generate a minimal view for the registered SuperModel.
113
+
114
+ Returns a flat list of {value, label} objects for dropdown/select usage.
115
+ Only returns active, non-deleted records.
116
+ """
117
+
118
+ model = config.model_class
119
+
120
+ class MinimalView(BaseView):
121
+ accepted_methods = ["GET"]
122
+
123
+ MinimalView.model = model
124
+
125
+ if config.permissions:
126
+ MinimalView.get_permissions = config.permissions
127
+
128
+ def get(self, *args, **kwargs):
129
+ from flask import jsonify
130
+ from sqlalchemy import func as sa_func
131
+
132
+ query = model.query.filter(model.deleted_at.is_(None))
133
+
134
+ # Filter only active records
135
+ if hasattr(model, "is_active"):
136
+ query = query.filter(model.is_active.is_(True))
137
+
138
+ instances = query.all()
139
+ schema = minimal_schema()
140
+ result = [schema.dump(instance) for instance in instances]
141
+ return jsonify(result), 200
142
+
143
+ MinimalView.get = get
144
+
145
+ view_name = f"{model.__name__}MinimalView"
146
+ MinimalView.__name__ = view_name
147
+ MinimalView.__qualname__ = view_name
148
+
149
+ return MinimalView
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dflango
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Django-like utilities for Flask applications
5
5
  Author-email: Martino Scarcia <martino.scarcia@hybrissoftware.it>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dflango"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "Django-like utilities for Flask applications"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes