djresttoolkit 0.5.0__py3-none-any.whl → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djresttoolkit
3
- Version: 0.5.0
3
+ Version: 0.6.0
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=SpAEE8mfd_cX8qgI-CeC4r1e17YLAwAlxeFgbK8gtE4,118
10
+ src/djresttoolkit/dbseed/models/_base_seed_model.py,sha256=DlbmdafLicKd_ja1AIvKKzyBxdnxdq23zPJwlqxiTl8,2187
11
+ src/djresttoolkit/dbseed/models/_gen.py,sha256=qBPQaLvh1rcEam0YmE4JBJqpa-Vv5IFlIIagkEMHDVw,206
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=tMpvvDx_es08GxYKgJ1umJddh_kmIB1Fy0J5Ich2hiE,4248
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.0.dist-info/METADATA,sha256=KKviIBCDtQeoaRNcs8w5zZvlTNVyK1KJXwqNo7hcogM,8501
31
+ djresttoolkit-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
32
+ djresttoolkit-0.6.0.dist-info/entry_points.txt,sha256=YMhfTF-7mYppO8QqqWnvR_hyMWvoYxD6XI94_ViFu3k,60
33
+ djresttoolkit-0.6.0.dist-info/licenses/LICENSE,sha256=8-oZM3yuuTRjySMbVKX9YXYA7Y4M_KhQNBYXPFjeWUo,1074
34
+ djresttoolkit-0.6.0.dist-info/RECORD,,
File without changes
@@ -0,0 +1,4 @@
1
+ from ._base_seed_model import BaseSeedModel
2
+ from ._gen import Field, Gen
3
+
4
+ __all__ = ["BaseSeedModel", "Gen", "Field"]
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from django.db.models import ForeignKey, ManyToManyField, Model, OneToOneField
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class BaseSeedModel(BaseModel):
10
+ """
11
+ Base class for all fake data models.
12
+ Each subclass must define a `model` attribute.
13
+ """
14
+
15
+ class Meta:
16
+ model: type[Model]
17
+
18
+ def __init_subclass__(cls, **kwargs: Any) -> None:
19
+ super().__init_subclass__(**kwargs)
20
+ if not hasattr(cls, "Meta") or not hasattr(cls.Meta, "model"):
21
+ raise TypeError(
22
+ f"{cls.__name__} must define a Meta class with a Django model"
23
+ )
24
+
25
+ @classmethod
26
+ def get_meta(cls) -> type[Meta]:
27
+ """Class-level access."""
28
+ return cls.Meta
29
+
30
+ @classmethod
31
+ def create_instance(cls) -> tuple[dict[str, Any], list[ManyToManyField[Any, Any]]]:
32
+ """Handle ForeignKey, OneToOneField and ManyToMany relationship."""
33
+
34
+ # dump pydantic model to python dict
35
+ data = cls().model_dump()
36
+
37
+ # Handle ForeignKey and OneToOneField
38
+ for field in cls.get_meta().model._meta.get_fields():
39
+ if isinstance(field, (ForeignKey, OneToOneField)):
40
+ rel_model = field.remote_field.model
41
+ if rel_model.objects.exists():
42
+ # For OneToOne, must ensure unique (pick unused relation)
43
+ if isinstance(field, OneToOneField):
44
+ used_ids = cls.get_meta().model.objects.values_list(
45
+ field.name, flat=True
46
+ )
47
+ available = rel_model.objects.exclude(pk__in=used_ids)
48
+ if available.exists():
49
+ data[field.name] = available.order_by("?").first()
50
+ else: # Normal ForeignKey
51
+ data[field.name] = rel_model.objects.order_by("?").first()
52
+
53
+ # Collect ManyToMany fields
54
+ m2m_fields: list[ManyToManyField[Any, Any]] = [
55
+ field
56
+ for field in cls.get_meta().model._meta.get_fields()
57
+ if isinstance(field, ManyToManyField)
58
+ ]
59
+ return data, m2m_fields
@@ -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
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 BaseSeedModel
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[BaseSeedModel]] = []
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, BaseSeedModel)
71
+ and attr is not BaseSeedModel
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_meta().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
+ )