djresttoolkit 0.5.0__py3-none-any.whl → 0.6.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djresttoolkit
3
- Version: 0.5.0
3
+ Version: 0.6.1
4
4
  Summary: A collection of Django and DRF utilities to simplify API development.
5
5
  Project-URL: Homepage, https://github.com/shaileshpandit141/djresttoolkit
6
6
  Project-URL: Documentation, https://shaileshpandit141.github.io/djresttoolkit
@@ -46,6 +46,7 @@ Classifier: Topic :: Software Development :: Libraries
46
46
  Classifier: Topic :: Utilities
47
47
  Classifier: Typing :: Typed
48
48
  Requires-Python: >=3.13
49
+ Requires-Dist: faker>=37.5.3
49
50
  Requires-Dist: pydantic>=2.11.7
50
51
  Provides-Extra: dev
51
52
  Requires-Dist: mypy; extra == 'dev'
@@ -5,10 +5,17 @@ src/djresttoolkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
5
5
  src/djresttoolkit/admin.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  src/djresttoolkit/apps.py,sha256=nKb5GUIEhAB3IL3lTmEXNc5XuvvaZupH-1CCuYKFrEQ,158
7
7
  src/djresttoolkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ src/djresttoolkit/dbseed/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ src/djresttoolkit/dbseed/models/__init__.py,sha256=B2WJqw2ncqf5nNhkM3XDFx13oNg2ccsULUQkFAjOyrY,105
10
+ src/djresttoolkit/dbseed/models/_gen.py,sha256=qBPQaLvh1rcEam0YmE4JBJqpa-Vv5IFlIIagkEMHDVw,206
11
+ src/djresttoolkit/dbseed/models/_seed_model.py,sha256=0cmbi0VNKjmJbwhjeCFsvb3iKYjok6TJOk6Y2MF_3N4,2443
8
12
  src/djresttoolkit/mail/__init__.py,sha256=tB9SdMlhfWQ640q4aobZ0H1c7fTWalpDL2I-onkr2VI,268
9
13
  src/djresttoolkit/mail/_email_sender.py,sha256=bPMqgD5HibJcOZgO6xxHOhdK9HEhnGNC6BoMPpo-h7k,3096
10
14
  src/djresttoolkit/mail/_models.py,sha256=of5KsLGvsN2OWgDYgdtLEijulg817TXgsLKuUdsnDQc,1447
11
15
  src/djresttoolkit/mail/_types.py,sha256=zf6CcXR1ei_UmZ1nLAJa378OAJ6ftnBICqEOkzXPNw8,646
16
+ src/djresttoolkit/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ src/djresttoolkit/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ src/djresttoolkit/management/commands/dbseed.py,sha256=vS-s_B8No2rTl_T7HuIBg7f_Yp_6lztvK7FbcaxTZaA,4227
12
19
  src/djresttoolkit/middlewares/__init__.py,sha256=GZHU3Yy4xXoEi62tHn0UJNxN6XgGM2_HES8Bt5AS5Lk,100
13
20
  src/djresttoolkit/middlewares/_response_time_middleware.py,sha256=1wCwdkW5Ng6HJo8zx0F7ylms84OGP-1K0kbyG6Vacuk,908
14
21
  src/djresttoolkit/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -20,8 +27,8 @@ src/djresttoolkit/throttling/_throttle_inspector.py,sha256=Kss6ZxKy-EXq9UGaGprGD
20
27
  src/djresttoolkit/views/__init__.py,sha256=XrxBrs6sH4HmUzp41omcmy_y94pSaXAVn01ttQ022-4,76
21
28
  src/djresttoolkit/views/_exceptions/__init__.py,sha256=DrCUxuPNyBR4WhzNutn5HDxLa--q51ykIxSG7_bFsOI,83
22
29
  src/djresttoolkit/views/_exceptions/_exception_handler.py,sha256=_o7If47bzWLl57LeSXSWsIDsJGo2RIpwYAwNQ-hsHVY,2839
23
- djresttoolkit-0.5.0.dist-info/METADATA,sha256=_XWebt-T78KglxuWs9jNFymCdcHhmbGQ1bJSlNFfh8A,8472
24
- djresttoolkit-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- djresttoolkit-0.5.0.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
26
- djresttoolkit-0.5.0.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
27
- djresttoolkit-0.5.0.dist-info/RECORD,,
30
+ djresttoolkit-0.6.1.dist-info/METADATA,sha256=doM3qba2_MGPAyrrfz3g79IjcNGaKyqiXYodOYFotso,8501
31
+ djresttoolkit-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
32
+ djresttoolkit-0.6.1.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
33
+ djresttoolkit-0.6.1.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
34
+ djresttoolkit-0.6.1.dist-info/RECORD,,
File without changes
@@ -0,0 +1,4 @@
1
+ from ._gen import Field, Gen
2
+ from ._seed_model import SeedModel
3
+
4
+ __all__ = ["SeedModel", "Gen", "Field"]
@@ -0,0 +1,11 @@
1
+ from faker import Faker
2
+ from pydantic import Field as PydField
3
+
4
+
5
+ class Generator:
6
+ @classmethod
7
+ def create_faker(cls) -> Faker:
8
+ return Faker()
9
+
10
+ Gen = Generator.create_faker()
11
+ Field = PydField
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ from django.db.models import ForeignKey, ManyToManyField, Model, OneToOneField
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class SeedModel(BaseModel):
10
+ """
11
+ Base class for all fake data models.
12
+ Each subclass must define a `__model__` attribute (a Django model).
13
+ """
14
+
15
+ __model__: ClassVar[type[Model] | None] = None
16
+
17
+ def __init_subclass__(cls, **kwargs: Any) -> None:
18
+ super().__init_subclass__(**kwargs)
19
+ if not hasattr(cls, "__model__"):
20
+ raise TypeError(
21
+ f"{cls.__name__} must define a `__model__` attribute "
22
+ f"pointing to a Django model"
23
+ )
24
+
25
+ @classmethod
26
+ def get_model(cls) -> type[Model]:
27
+ """Class-level access to the Django model."""
28
+ if cls.__model__ is None:
29
+ raise ValueError(
30
+ f"{cls.__name__} has no `__model__` defined. "
31
+ "Please set it to a Django model class."
32
+ )
33
+ return cls.__model__
34
+
35
+ @classmethod
36
+ def create_instance(cls) -> tuple[dict[str, Any], list[ManyToManyField[Any, Any]]]:
37
+ """Handle ForeignKey, OneToOneField and ManyToMany relationship."""
38
+
39
+ # dump pydantic model to python dict
40
+ data = cls().model_dump()
41
+
42
+ # Handle ForeignKey and OneToOneField
43
+ for field in cls.get_model()._meta.get_fields():
44
+ if isinstance(field, (ForeignKey, OneToOneField)):
45
+ rel_model = field.remote_field.model
46
+ if rel_model.objects.exists():
47
+ # For OneToOne, must ensure unique (pick unused relation)
48
+ if isinstance(field, OneToOneField):
49
+ used_ids = cls.get_model().objects.values_list(
50
+ field.name, flat=True
51
+ )
52
+ available = rel_model.objects.exclude(pk__in=used_ids)
53
+ if available.exists():
54
+ data[field.name] = available.order_by("?").first()
55
+ else: # Normal ForeignKey
56
+ data[field.name] = rel_model.objects.order_by("?").first()
57
+
58
+ # Collect ManyToMany fields
59
+ m2m_fields: list[ManyToManyField[Any, Any]] = [
60
+ field
61
+ for field in cls.get_model()._meta.get_fields()
62
+ if isinstance(field, ManyToManyField)
63
+ ]
64
+ return data, m2m_fields
65
+
File without changes
File without changes
@@ -0,0 +1,115 @@
1
+ import pkgutil
2
+ from importlib import import_module
3
+ from types import ModuleType
4
+ from typing import Any
5
+
6
+ from django.apps import apps
7
+ from django.core.management.base import BaseCommand, CommandParser
8
+ from django.db import transaction
9
+ from django.db.models import Model, QuerySet
10
+
11
+ from djresttoolkit.dbseed.models import SeedModel
12
+
13
+
14
+ class Command(BaseCommand):
15
+ help = "Seed the database with fake data for any Django model"
16
+
17
+ def add_arguments(self, parser: CommandParser) -> None:
18
+ """Add command arguments."""
19
+ parser.add_argument(
20
+ "--count",
21
+ type=int,
22
+ default=5,
23
+ help="Number of records per model",
24
+ )
25
+ parser.add_argument(
26
+ "--model",
27
+ type=str,
28
+ default=None,
29
+ help="Specific model name to seed (e.g., User, Product)",
30
+ )
31
+ parser.add_argument(
32
+ "--seed",
33
+ type=int,
34
+ default=None,
35
+ help="Optional Faker seed for reproducible data",
36
+ )
37
+
38
+ def handle(self, *args: Any, **options: Any) -> None:
39
+ """Handle ForeignKey, OneToOneField and ManyToMany relationship."""
40
+
41
+ count = options["count"]
42
+ model_name = options["model"]
43
+ seed = options["seed"]
44
+
45
+ if seed is not None:
46
+ from faker import Faker
47
+
48
+ faker = Faker()
49
+ faker.seed_instance(seed)
50
+
51
+ seed_model_classes: list[type[SeedModel]] = []
52
+
53
+ # Discover all dbseed dirs in installed apps
54
+ for app_config in apps.get_app_configs():
55
+ try:
56
+ module: ModuleType = import_module(f"{app_config.name}.dbseed")
57
+ except ModuleNotFoundError:
58
+ continue # app has no dbseed dir
59
+
60
+ # Iterate over modules in the dbseed package
61
+ for _, name, ispkg in pkgutil.iter_modules(module.__path__):
62
+ if ispkg:
63
+ continue
64
+
65
+ submodule = import_module(f"{app_config.name}.dbseed.{name}")
66
+ for attr_name in dir(submodule):
67
+ attr = getattr(submodule, attr_name)
68
+ if (
69
+ isinstance(attr, type)
70
+ and issubclass(attr, SeedModel)
71
+ and attr is not SeedModel
72
+ ):
73
+ # Filter by model name if provided
74
+ if (
75
+ not model_name
76
+ or model_name.lower()
77
+ == attr.__name__.replace("DBSeedModel", "").lower()
78
+ ):
79
+ seed_model_classes.append(attr)
80
+
81
+ if not seed_model_classes:
82
+ self.stdout.write(self.style.WARNING("No matching dbseed models found."))
83
+ return None
84
+
85
+ # Generate fake data for each discovered dbseed model
86
+ for dbseed_cls in seed_model_classes:
87
+ django_model = dbseed_cls.get_model()
88
+ created_count: int = 0
89
+
90
+ for _ in range(count):
91
+ try:
92
+ with transaction.atomic():
93
+ data, m2m_fields = dbseed_cls.create_instance()
94
+ obj = django_model.objects.create(**data)
95
+ created_count += 1
96
+
97
+ # Assign ManyToMany fields
98
+ for m2m_field in m2m_fields:
99
+ rel_model = m2m_field.remote_field.model
100
+ related_instances: QuerySet[Model] | list[Any] = (
101
+ rel_model.objects.order_by("?")[:2]
102
+ if rel_model.objects.exists()
103
+ else []
104
+ )
105
+ getattr(obj, m2m_field.name).set(related_instances)
106
+ except Exception as error:
107
+ self.stderr.write(
108
+ f"Error creating {django_model.__name__} instance: {error}"
109
+ )
110
+ continue
111
+ self.stdout.write(
112
+ self.style.SUCCESS(
113
+ f"{created_count} records inserted for {django_model.__name__}"
114
+ )
115
+ )