core-framework 0.12.7__py3-none-any.whl → 0.12.9__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.
@@ -0,0 +1,393 @@
1
+ """
2
+ Data factories for generating test data.
3
+
4
+ Provides a factory pattern for creating test instances with fake data.
5
+
6
+ Usage:
7
+ # Define a factory
8
+ class UserFactory(Factory):
9
+ model = User
10
+
11
+ @classmethod
12
+ def build(cls, **overrides):
13
+ return {
14
+ "email": fake.email(),
15
+ "name": fake.name(),
16
+ **overrides,
17
+ }
18
+
19
+ # Use in tests
20
+ async def test_user_creation(db):
21
+ user = await UserFactory.create(db)
22
+ assert user.id is not None
23
+
24
+ users = await UserFactory.create_batch(db, 5)
25
+ assert len(users) == 5
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ from typing import TypeVar, Generic, Any, TYPE_CHECKING
32
+ from uuid import uuid4
33
+
34
+ if TYPE_CHECKING:
35
+ from sqlalchemy.ext.asyncio import AsyncSession
36
+
37
+ logger = logging.getLogger("core.testing")
38
+
39
+ # Try to import faker, provide fallback
40
+ try:
41
+ from faker import Faker
42
+ fake = Faker()
43
+ except ImportError:
44
+ # Minimal fake data generator if faker not installed
45
+ class MinimalFaker:
46
+ """Minimal faker replacement when faker is not installed."""
47
+
48
+ _counter = 0
49
+
50
+ def email(self) -> str:
51
+ self._counter += 1
52
+ return f"user{self._counter}@example.com"
53
+
54
+ def name(self) -> str:
55
+ self._counter += 1
56
+ return f"User {self._counter}"
57
+
58
+ def first_name(self) -> str:
59
+ return "John"
60
+
61
+ def last_name(self) -> str:
62
+ return "Doe"
63
+
64
+ def text(self, max_nb_chars: int = 200) -> str:
65
+ return "Lorem ipsum dolor sit amet." * (max_nb_chars // 30 + 1)
66
+
67
+ def sentence(self) -> str:
68
+ return "This is a test sentence."
69
+
70
+ def paragraph(self) -> str:
71
+ return "This is a test paragraph with multiple sentences. It contains some text for testing purposes."
72
+
73
+ def url(self) -> str:
74
+ self._counter += 1
75
+ return f"https://example.com/{self._counter}"
76
+
77
+ def uuid4(self) -> str:
78
+ return str(uuid4())
79
+
80
+ def random_int(self, min: int = 0, max: int = 9999) -> int:
81
+ import random
82
+ return random.randint(min, max)
83
+
84
+ def boolean(self) -> bool:
85
+ import random
86
+ return random.choice([True, False])
87
+
88
+ def date_this_year(self) -> str:
89
+ from datetime import date
90
+ return date.today().isoformat()
91
+
92
+ def date_time_this_year(self):
93
+ from datetime import datetime
94
+ return datetime.now()
95
+
96
+ def company(self) -> str:
97
+ self._counter += 1
98
+ return f"Company {self._counter}"
99
+
100
+ def phone_number(self) -> str:
101
+ self._counter += 1
102
+ return f"+1-555-{self._counter:04d}"
103
+
104
+ def address(self) -> str:
105
+ return "123 Test Street, Test City, TC 12345"
106
+
107
+ fake = MinimalFaker()
108
+ logger.warning(
109
+ "faker not installed. Using minimal fake data generator. "
110
+ "Install with: pip install faker"
111
+ )
112
+
113
+
114
+ T = TypeVar("T")
115
+
116
+
117
+ class Factory(Generic[T]):
118
+ """
119
+ Base factory for creating test instances.
120
+
121
+ Subclass this to create factories for your models.
122
+
123
+ Example:
124
+ class UserFactory(Factory):
125
+ model = User
126
+
127
+ @classmethod
128
+ def build(cls, **overrides):
129
+ return {
130
+ "email": fake.email(),
131
+ "name": fake.name(),
132
+ "is_active": True,
133
+ **overrides,
134
+ }
135
+
136
+ # Build dict (no database)
137
+ data = UserFactory.build(name="Custom Name")
138
+
139
+ # Create instance (saves to database)
140
+ user = await UserFactory.create(db)
141
+
142
+ # Create multiple
143
+ users = await UserFactory.create_batch(db, 10)
144
+
145
+ Attributes:
146
+ model: The model class to create instances of
147
+ """
148
+
149
+ model: type[T]
150
+
151
+ @classmethod
152
+ def build(cls, **overrides) -> dict[str, Any]:
153
+ """
154
+ Build a dict of attributes without saving.
155
+
156
+ Override this method in subclasses to define default values.
157
+
158
+ Args:
159
+ **overrides: Values to override defaults
160
+
161
+ Returns:
162
+ Dict of model attributes
163
+ """
164
+ raise NotImplementedError(
165
+ f"{cls.__name__} must implement build() method"
166
+ )
167
+
168
+ @classmethod
169
+ async def create(
170
+ cls,
171
+ db: "AsyncSession",
172
+ **overrides,
173
+ ) -> T:
174
+ """
175
+ Create and save an instance to the database.
176
+
177
+ Args:
178
+ db: Database session
179
+ **overrides: Values to override defaults
180
+
181
+ Returns:
182
+ Created model instance
183
+ """
184
+ data = cls.build(**overrides)
185
+ instance = cls.model(**data)
186
+
187
+ db.add(instance)
188
+ await db.commit()
189
+ await db.refresh(instance)
190
+
191
+ logger.debug(f"Factory created: {cls.model.__name__}")
192
+ return instance
193
+
194
+ @classmethod
195
+ async def create_batch(
196
+ cls,
197
+ db: "AsyncSession",
198
+ count: int,
199
+ **overrides,
200
+ ) -> list[T]:
201
+ """
202
+ Create multiple instances.
203
+
204
+ Args:
205
+ db: Database session
206
+ count: Number of instances to create
207
+ **overrides: Values to override defaults for all instances
208
+
209
+ Returns:
210
+ List of created instances
211
+ """
212
+ instances = []
213
+ for _ in range(count):
214
+ instance = await cls.create(db, **overrides)
215
+ instances.append(instance)
216
+
217
+ logger.debug(f"Factory created batch: {count} x {cls.model.__name__}")
218
+ return instances
219
+
220
+ @classmethod
221
+ def build_batch(cls, count: int, **overrides) -> list[dict[str, Any]]:
222
+ """
223
+ Build multiple dicts without saving.
224
+
225
+ Args:
226
+ count: Number of dicts to build
227
+ **overrides: Values to override defaults
228
+
229
+ Returns:
230
+ List of dicts
231
+ """
232
+ return [cls.build(**overrides) for _ in range(count)]
233
+
234
+
235
+ class UserFactory(Factory):
236
+ """
237
+ Factory for creating test users.
238
+
239
+ Works with AbstractUser-based models.
240
+
241
+ Example:
242
+ # Create user with random data
243
+ user = await UserFactory.create(db)
244
+
245
+ # Create with specific email
246
+ admin = await UserFactory.create(db, email="admin@example.com", is_superuser=True)
247
+
248
+ # Create batch
249
+ users = await UserFactory.create_batch(db, 10)
250
+ """
251
+
252
+ model: type = None # Set dynamically
253
+ _default_password: str = "TestPass123!"
254
+
255
+ @classmethod
256
+ def _get_model(cls):
257
+ """Get user model from auth config."""
258
+ if cls.model is not None:
259
+ return cls.model
260
+
261
+ try:
262
+ from core.auth.models import get_user_model
263
+ return get_user_model()
264
+ except Exception:
265
+ pass
266
+
267
+ try:
268
+ from core.auth.models import AbstractUser
269
+ return AbstractUser
270
+ except Exception:
271
+ raise RuntimeError(
272
+ "Could not determine User model. "
273
+ "Set UserFactory.model = YourUserModel"
274
+ )
275
+
276
+ @classmethod
277
+ def build(cls, **overrides) -> dict[str, Any]:
278
+ """Build user data dict."""
279
+ password = overrides.pop("password", cls._default_password)
280
+
281
+ data = {
282
+ "email": fake.email(),
283
+ "is_active": True,
284
+ "is_superuser": False,
285
+ "is_staff": False,
286
+ }
287
+ data.update(overrides)
288
+
289
+ # Handle password hashing
290
+ if "password_hash" not in data:
291
+ try:
292
+ from core.auth.hashers import get_hasher
293
+ hasher = get_hasher()
294
+ data["password_hash"] = hasher.hash(password)
295
+ except Exception:
296
+ # Fallback: assume model handles password
297
+ data["_password"] = password
298
+
299
+ return data
300
+
301
+ @classmethod
302
+ async def create(
303
+ cls,
304
+ db: "AsyncSession",
305
+ **overrides,
306
+ ) -> Any:
307
+ """Create and save a user."""
308
+ model = cls._get_model()
309
+
310
+ data = cls.build(**overrides)
311
+
312
+ # Handle password separately if model has set_password
313
+ password = data.pop("_password", None)
314
+
315
+ instance = model(**data)
316
+
317
+ if password and hasattr(instance, "set_password"):
318
+ instance.set_password(password)
319
+
320
+ db.add(instance)
321
+ await db.commit()
322
+ await db.refresh(instance)
323
+
324
+ return instance
325
+
326
+
327
+ # Additional common factories
328
+
329
+ class SequenceFactory:
330
+ """
331
+ Generate sequential values for unique fields.
332
+
333
+ Example:
334
+ seq = SequenceFactory("user_{n}@example.com")
335
+ seq.next() # "user_1@example.com"
336
+ seq.next() # "user_2@example.com"
337
+ """
338
+
339
+ def __init__(self, template: str, start: int = 1) -> None:
340
+ """
341
+ Initialize sequence.
342
+
343
+ Args:
344
+ template: String template with {n} placeholder
345
+ start: Starting number
346
+ """
347
+ self.template = template
348
+ self.counter = start
349
+
350
+ def next(self) -> str:
351
+ """Get next value in sequence."""
352
+ value = self.template.format(n=self.counter)
353
+ self.counter += 1
354
+ return value
355
+
356
+ def reset(self, start: int = 1) -> None:
357
+ """Reset counter."""
358
+ self.counter = start
359
+
360
+
361
+ class LazyAttribute:
362
+ """
363
+ Lazily compute attribute value.
364
+
365
+ Example:
366
+ class PostFactory(Factory):
367
+ model = Post
368
+
369
+ @classmethod
370
+ def build(cls, **overrides):
371
+ return {
372
+ "title": fake.sentence(),
373
+ "slug": LazyAttribute(lambda obj: slugify(obj["title"])),
374
+ **overrides,
375
+ }
376
+ """
377
+
378
+ def __init__(self, func) -> None:
379
+ self.func = func
380
+
381
+ def __call__(self, obj: dict) -> Any:
382
+ return self.func(obj)
383
+
384
+
385
+ def resolve_lazy_attributes(data: dict) -> dict:
386
+ """Resolve any LazyAttribute values in a dict."""
387
+ result = {}
388
+ for key, value in data.items():
389
+ if isinstance(value, LazyAttribute):
390
+ result[key] = value(data)
391
+ else:
392
+ result[key] = value
393
+ return result