dataforge-py 0.2.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.
Files changed (145) hide show
  1. dataforge/__init__.py +20 -0
  2. dataforge/backend.py +147 -0
  3. dataforge/cli.py +166 -0
  4. dataforge/core.py +1169 -0
  5. dataforge/locales/__init__.py +1 -0
  6. dataforge/locales/ar_SA/__init__.py +1 -0
  7. dataforge/locales/ar_SA/address.py +128 -0
  8. dataforge/locales/ar_SA/company.py +183 -0
  9. dataforge/locales/ar_SA/internet.py +25 -0
  10. dataforge/locales/ar_SA/person.py +217 -0
  11. dataforge/locales/ar_SA/phone.py +15 -0
  12. dataforge/locales/de_DE/__init__.py +1 -0
  13. dataforge/locales/de_DE/address.py +148 -0
  14. dataforge/locales/de_DE/company.py +125 -0
  15. dataforge/locales/de_DE/internet.py +32 -0
  16. dataforge/locales/de_DE/person.py +212 -0
  17. dataforge/locales/de_DE/phone.py +17 -0
  18. dataforge/locales/en_AU/__init__.py +1 -0
  19. dataforge/locales/en_AU/address.py +231 -0
  20. dataforge/locales/en_AU/company.py +193 -0
  21. dataforge/locales/en_AU/internet.py +34 -0
  22. dataforge/locales/en_AU/person.py +370 -0
  23. dataforge/locales/en_AU/phone.py +16 -0
  24. dataforge/locales/en_CA/__init__.py +1 -0
  25. dataforge/locales/en_CA/address.py +276 -0
  26. dataforge/locales/en_CA/company.py +193 -0
  27. dataforge/locales/en_CA/internet.py +34 -0
  28. dataforge/locales/en_CA/person.py +377 -0
  29. dataforge/locales/en_CA/phone.py +15 -0
  30. dataforge/locales/en_GB/__init__.py +1 -0
  31. dataforge/locales/en_GB/address.py +312 -0
  32. dataforge/locales/en_GB/company.py +196 -0
  33. dataforge/locales/en_GB/internet.py +34 -0
  34. dataforge/locales/en_GB/person.py +372 -0
  35. dataforge/locales/en_GB/phone.py +15 -0
  36. dataforge/locales/en_US/__init__.py +1 -0
  37. dataforge/locales/en_US/address.py +268 -0
  38. dataforge/locales/en_US/company.py +191 -0
  39. dataforge/locales/en_US/internet.py +34 -0
  40. dataforge/locales/en_US/person.py +370 -0
  41. dataforge/locales/en_US/phone.py +15 -0
  42. dataforge/locales/es_ES/__init__.py +1 -0
  43. dataforge/locales/es_ES/address.py +151 -0
  44. dataforge/locales/es_ES/company.py +125 -0
  45. dataforge/locales/es_ES/internet.py +30 -0
  46. dataforge/locales/es_ES/person.py +207 -0
  47. dataforge/locales/es_ES/phone.py +15 -0
  48. dataforge/locales/fr_FR/__init__.py +1 -0
  49. dataforge/locales/fr_FR/address.py +145 -0
  50. dataforge/locales/fr_FR/company.py +125 -0
  51. dataforge/locales/fr_FR/internet.py +30 -0
  52. dataforge/locales/fr_FR/person.py +212 -0
  53. dataforge/locales/fr_FR/phone.py +15 -0
  54. dataforge/locales/hi_IN/__init__.py +1 -0
  55. dataforge/locales/hi_IN/address.py +177 -0
  56. dataforge/locales/hi_IN/company.py +191 -0
  57. dataforge/locales/hi_IN/internet.py +26 -0
  58. dataforge/locales/hi_IN/person.py +218 -0
  59. dataforge/locales/hi_IN/phone.py +21 -0
  60. dataforge/locales/it_IT/__init__.py +1 -0
  61. dataforge/locales/it_IT/address.py +218 -0
  62. dataforge/locales/it_IT/company.py +151 -0
  63. dataforge/locales/it_IT/internet.py +31 -0
  64. dataforge/locales/it_IT/person.py +187 -0
  65. dataforge/locales/it_IT/phone.py +15 -0
  66. dataforge/locales/ja_JP/__init__.py +1 -0
  67. dataforge/locales/ja_JP/address.py +174 -0
  68. dataforge/locales/ja_JP/company.py +121 -0
  69. dataforge/locales/ja_JP/internet.py +30 -0
  70. dataforge/locales/ja_JP/person.py +207 -0
  71. dataforge/locales/ja_JP/phone.py +18 -0
  72. dataforge/locales/ko_KR/__init__.py +1 -0
  73. dataforge/locales/ko_KR/address.py +121 -0
  74. dataforge/locales/ko_KR/company.py +151 -0
  75. dataforge/locales/ko_KR/internet.py +30 -0
  76. dataforge/locales/ko_KR/person.py +157 -0
  77. dataforge/locales/ko_KR/phone.py +26 -0
  78. dataforge/locales/nl_NL/__init__.py +1 -0
  79. dataforge/locales/nl_NL/address.py +152 -0
  80. dataforge/locales/nl_NL/company.py +182 -0
  81. dataforge/locales/nl_NL/internet.py +41 -0
  82. dataforge/locales/nl_NL/person.py +218 -0
  83. dataforge/locales/nl_NL/phone.py +19 -0
  84. dataforge/locales/pl_PL/__init__.py +1 -0
  85. dataforge/locales/pl_PL/address.py +140 -0
  86. dataforge/locales/pl_PL/company.py +183 -0
  87. dataforge/locales/pl_PL/internet.py +36 -0
  88. dataforge/locales/pl_PL/person.py +217 -0
  89. dataforge/locales/pl_PL/phone.py +15 -0
  90. dataforge/locales/pt_BR/__init__.py +1 -0
  91. dataforge/locales/pt_BR/address.py +127 -0
  92. dataforge/locales/pt_BR/company.py +151 -0
  93. dataforge/locales/pt_BR/internet.py +31 -0
  94. dataforge/locales/pt_BR/person.py +187 -0
  95. dataforge/locales/pt_BR/phone.py +15 -0
  96. dataforge/locales/ru_RU/__init__.py +1 -0
  97. dataforge/locales/ru_RU/address.py +156 -0
  98. dataforge/locales/ru_RU/company.py +168 -0
  99. dataforge/locales/ru_RU/internet.py +26 -0
  100. dataforge/locales/ru_RU/person.py +218 -0
  101. dataforge/locales/ru_RU/phone.py +16 -0
  102. dataforge/locales/zh_CN/__init__.py +1 -0
  103. dataforge/locales/zh_CN/address.py +141 -0
  104. dataforge/locales/zh_CN/company.py +151 -0
  105. dataforge/locales/zh_CN/internet.py +30 -0
  106. dataforge/locales/zh_CN/person.py +157 -0
  107. dataforge/locales/zh_CN/phone.py +25 -0
  108. dataforge/providers/__init__.py +1 -0
  109. dataforge/providers/address.py +460 -0
  110. dataforge/providers/ai_chat.py +170 -0
  111. dataforge/providers/ai_prompt.py +447 -0
  112. dataforge/providers/automotive.py +416 -0
  113. dataforge/providers/barcode.py +149 -0
  114. dataforge/providers/base.py +34 -0
  115. dataforge/providers/color.py +247 -0
  116. dataforge/providers/company.py +144 -0
  117. dataforge/providers/crypto.py +105 -0
  118. dataforge/providers/datetime.py +397 -0
  119. dataforge/providers/ecommerce.py +316 -0
  120. dataforge/providers/education.py +234 -0
  121. dataforge/providers/file.py +271 -0
  122. dataforge/providers/finance.py +545 -0
  123. dataforge/providers/geo.py +332 -0
  124. dataforge/providers/government.py +114 -0
  125. dataforge/providers/internet.py +351 -0
  126. dataforge/providers/llm.py +726 -0
  127. dataforge/providers/lorem.py +241 -0
  128. dataforge/providers/medical.py +364 -0
  129. dataforge/providers/misc.py +196 -0
  130. dataforge/providers/network.py +283 -0
  131. dataforge/providers/payment.py +300 -0
  132. dataforge/providers/person.py +195 -0
  133. dataforge/providers/phone.py +87 -0
  134. dataforge/providers/profile.py +265 -0
  135. dataforge/providers/science.py +365 -0
  136. dataforge/providers/text.py +365 -0
  137. dataforge/py.typed +0 -0
  138. dataforge/pytest_plugin.py +80 -0
  139. dataforge/registry.py +164 -0
  140. dataforge/schema.py +772 -0
  141. dataforge/unique.py +171 -0
  142. dataforge_py-0.2.0.dist-info/METADATA +964 -0
  143. dataforge_py-0.2.0.dist-info/RECORD +145 -0
  144. dataforge_py-0.2.0.dist-info/WHEEL +4 -0
  145. dataforge_py-0.2.0.dist-info/entry_points.txt +35 -0
@@ -0,0 +1,397 @@
1
+ """DateTime provider — generates fake dates, times, and datetimes.
2
+
3
+ This provider is locale-independent — it uses Python's datetime module
4
+ directly and generates values within configurable ranges.
5
+ """
6
+
7
+ import datetime as _dt
8
+ from typing import Literal, overload
9
+
10
+ from dataforge.providers.base import BaseProvider
11
+
12
+ # Epoch boundaries (as ordinals for fast random int generation)
13
+ _MIN_DATE = _dt.date(1970, 1, 1)
14
+ _MAX_DATE = _dt.date(2030, 12, 31)
15
+ _MIN_ORDINAL = _MIN_DATE.toordinal()
16
+ _MAX_ORDINAL = _MAX_DATE.toordinal()
17
+ _SECONDS_IN_DAY = 86400
18
+ _EPOCH_ORDINAL = _MIN_DATE.toordinal() # cached for unix_timestamp()
19
+
20
+ # Pre-computed default timestamp range — avoids .toordinal() on every call
21
+ _MIN_TIMESTAMP = 0 # (_MIN_ORDINAL - _EPOCH_ORDINAL) * _SECONDS_IN_DAY
22
+ _MAX_TIMESTAMP = (_MAX_ORDINAL - _EPOCH_ORDINAL + 1) * _SECONDS_IN_DAY - 1
23
+
24
+ # IANA timezone names — common subset for fast random selection
25
+ _TIMEZONES: tuple[str, ...] = (
26
+ "UTC",
27
+ "US/Eastern",
28
+ "US/Central",
29
+ "US/Mountain",
30
+ "US/Pacific",
31
+ "US/Alaska",
32
+ "US/Hawaii",
33
+ "Canada/Eastern",
34
+ "Canada/Central",
35
+ "Canada/Pacific",
36
+ "Europe/London",
37
+ "Europe/Paris",
38
+ "Europe/Berlin",
39
+ "Europe/Madrid",
40
+ "Europe/Rome",
41
+ "Europe/Amsterdam",
42
+ "Europe/Brussels",
43
+ "Europe/Vienna",
44
+ "Europe/Warsaw",
45
+ "Europe/Moscow",
46
+ "Europe/Istanbul",
47
+ "Europe/Athens",
48
+ "Europe/Helsinki",
49
+ "Europe/Stockholm",
50
+ "Europe/Oslo",
51
+ "Europe/Zurich",
52
+ "Asia/Tokyo",
53
+ "Asia/Shanghai",
54
+ "Asia/Hong_Kong",
55
+ "Asia/Seoul",
56
+ "Asia/Singapore",
57
+ "Asia/Dubai",
58
+ "Asia/Kolkata",
59
+ "Asia/Bangkok",
60
+ "Asia/Jakarta",
61
+ "Asia/Karachi",
62
+ "Asia/Riyadh",
63
+ "Asia/Taipei",
64
+ "Australia/Sydney",
65
+ "Australia/Melbourne",
66
+ "Australia/Perth",
67
+ "Pacific/Auckland",
68
+ "America/Sao_Paulo",
69
+ "America/Mexico_City",
70
+ "America/Buenos_Aires",
71
+ "America/Bogota",
72
+ "America/Lima",
73
+ "Africa/Cairo",
74
+ "Africa/Lagos",
75
+ "Africa/Johannesburg",
76
+ "Africa/Nairobi",
77
+ )
78
+
79
+
80
+ class DateTimeProvider(BaseProvider):
81
+ """Generates fake dates, times, datetimes, and dates of birth.
82
+
83
+ This provider does **not** require locale data — it uses Python's
84
+ ``datetime`` module directly.
85
+
86
+ Parameters
87
+ ----------
88
+ engine : RandomEngine
89
+ The shared random engine instance.
90
+ """
91
+
92
+ __slots__ = ()
93
+
94
+ _provider_name = "dt"
95
+ _locale_modules = ()
96
+ _field_map = {
97
+ "date": "date",
98
+ "time": "time",
99
+ "datetime": "datetime",
100
+ "date_of_birth": "date_of_birth",
101
+ "dob": "date_of_birth",
102
+ "timezone": "timezone",
103
+ "unix_timestamp": "unix_timestamp",
104
+ }
105
+
106
+ # ------------------------------------------------------------------
107
+ # Scalar helpers
108
+ # ------------------------------------------------------------------
109
+
110
+ def _one_date(
111
+ self,
112
+ start: _dt.date = _MIN_DATE,
113
+ end: _dt.date = _MAX_DATE,
114
+ ) -> _dt.date:
115
+ # Use pre-computed ordinals for default range to avoid
116
+ # .toordinal() per call.
117
+ if start is _MIN_DATE and end is _MAX_DATE:
118
+ ordinal = self._engine.random_int(_MIN_ORDINAL, _MAX_ORDINAL)
119
+ else:
120
+ ordinal = self._engine.random_int(start.toordinal(), end.toordinal())
121
+ return _dt.date.fromordinal(ordinal)
122
+
123
+ @staticmethod
124
+ def _date_to_iso(d: _dt.date) -> str:
125
+ """Format date as ISO 8601 string without ``strftime`` overhead."""
126
+ return f"{d.year:04d}-{d.month:02d}-{d.day:02d}"
127
+
128
+ def _one_time(self) -> _dt.time:
129
+ total_seconds = self._engine.random_int(0, _SECONDS_IN_DAY - 1)
130
+ hour = total_seconds // 3600
131
+ minute = (total_seconds % 3600) // 60
132
+ second = total_seconds % 60
133
+ return _dt.time(hour, minute, second)
134
+
135
+ @staticmethod
136
+ def _time_to_hms(t: _dt.time) -> str:
137
+ """Format time as ``HH:MM:SS`` without ``strftime`` overhead."""
138
+ return f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}"
139
+
140
+ def _one_time_str(self) -> str:
141
+ """Generate a random time as ``HH:MM:SS`` string — fast path.
142
+
143
+ Bypasses ``_dt.time`` object creation entirely by using
144
+ ``divmod`` arithmetic directly on the random seconds value.
145
+ """
146
+ total = self._engine.random_int(0, _SECONDS_IN_DAY - 1)
147
+ h, rem = divmod(total, 3600)
148
+ m, s = divmod(rem, 60)
149
+ return f"{h:02d}:{m:02d}:{s:02d}"
150
+
151
+ def _one_datetime(
152
+ self,
153
+ start: _dt.date = _MIN_DATE,
154
+ end: _dt.date = _MAX_DATE,
155
+ ) -> _dt.datetime:
156
+ d = self._one_date(start, end)
157
+ t = self._one_time()
158
+ return _dt.datetime.combine(d, t)
159
+
160
+ def _one_date_of_birth(self, min_age: int = 18, max_age: int = 80) -> _dt.date:
161
+ today = _dt.date.today()
162
+ start = today.replace(year=today.year - max_age)
163
+ end = today.replace(year=today.year - min_age)
164
+ return self._one_date(start, end)
165
+
166
+ # ------------------------------------------------------------------
167
+ # Public API
168
+ # ------------------------------------------------------------------
169
+
170
+ @overload
171
+ def date(self) -> str: ...
172
+ @overload
173
+ def date(self, count: Literal[1]) -> str: ...
174
+ @overload
175
+ def date(self, count: int) -> str | list[str]: ...
176
+ def date(
177
+ self,
178
+ count: int = 1,
179
+ fmt: str = "%Y-%m-%d",
180
+ start: _dt.date | None = None,
181
+ end: _dt.date | None = None,
182
+ ) -> str | list[str]:
183
+ """Generate a random date string.
184
+
185
+ Parameters
186
+ ----------
187
+ count : int
188
+ Number of dates to generate.
189
+ fmt : str
190
+ strftime format string.
191
+ start : datetime.date | None
192
+ Earliest date (default: 1970-01-01).
193
+ end : datetime.date | None
194
+ Latest date (default: 2030-12-31).
195
+ """
196
+ s = start or _MIN_DATE
197
+ e = end or _MAX_DATE
198
+ # Fast path: default ISO format avoids expensive strftime
199
+ if fmt == "%Y-%m-%d":
200
+ _iso = self._date_to_iso
201
+ # Pre-compute ordinals once for the batch instead of
202
+ # per-item .toordinal() calls inside _one_date().
203
+ if s is _MIN_DATE and e is _MAX_DATE:
204
+ s_ord, e_ord = _MIN_ORDINAL, _MAX_ORDINAL
205
+ else:
206
+ s_ord, e_ord = s.toordinal(), e.toordinal()
207
+ _ri = self._engine.random_int
208
+ _from_ord = _dt.date.fromordinal
209
+ if count == 1:
210
+ return _iso(_from_ord(_ri(s_ord, e_ord)))
211
+ return [_iso(_from_ord(_ri(s_ord, e_ord))) for _ in range(count)]
212
+ if count == 1:
213
+ return self._one_date(s, e).strftime(fmt)
214
+ return [self._one_date(s, e).strftime(fmt) for _ in range(count)]
215
+
216
+ @overload
217
+ def time(self) -> str: ...
218
+ @overload
219
+ def time(self, count: Literal[1]) -> str: ...
220
+ @overload
221
+ def time(self, count: int) -> str | list[str]: ...
222
+ def time(self, count: int = 1, fmt: str = "%H:%M:%S") -> str | list[str]:
223
+ """Generate a random time string.
224
+
225
+ Parameters
226
+ ----------
227
+ count : int
228
+ Number of times to generate.
229
+ fmt : str
230
+ strftime format string.
231
+ """
232
+ # Fast path: default HH:MM:SS format — skip _dt.time object
233
+ if fmt == "%H:%M:%S":
234
+ if count == 1:
235
+ return self._one_time_str()
236
+ # Inlined batch: avoid method call overhead per item
237
+ _ri = self._engine.random_int
238
+ result: list[str] = []
239
+ for _ in range(count):
240
+ total = _ri(0, _SECONDS_IN_DAY - 1)
241
+ h, rem = divmod(total, 3600)
242
+ m, s = divmod(rem, 60)
243
+ result.append(f"{h:02d}:{m:02d}:{s:02d}")
244
+ return result
245
+ if count == 1:
246
+ return self._one_time().strftime(fmt)
247
+ return [self._one_time().strftime(fmt) for _ in range(count)]
248
+
249
+ @overload
250
+ def datetime(self) -> str: ...
251
+ @overload
252
+ def datetime(self, count: Literal[1]) -> str: ...
253
+ @overload
254
+ def datetime(self, count: int) -> str | list[str]: ...
255
+ def datetime(
256
+ self,
257
+ count: int = 1,
258
+ fmt: str = "%Y-%m-%d %H:%M:%S",
259
+ start: _dt.date | None = None,
260
+ end: _dt.date | None = None,
261
+ ) -> str | list[str]:
262
+ """Generate a random datetime string.
263
+
264
+ Parameters
265
+ ----------
266
+ count : int
267
+ Number of datetimes to generate.
268
+ fmt : str
269
+ strftime format string.
270
+ start : datetime.date | None
271
+ Earliest date (default: 1970-01-01).
272
+ end : datetime.date | None
273
+ Latest date (default: 2030-12-31).
274
+ """
275
+ s = start or _MIN_DATE
276
+ e = end or _MAX_DATE
277
+ if count == 1:
278
+ return self._one_datetime(s, e).strftime(fmt)
279
+ return [self._one_datetime(s, e).strftime(fmt) for _ in range(count)]
280
+
281
+ @overload
282
+ def date_of_birth(self) -> str: ...
283
+ @overload
284
+ def date_of_birth(self, count: Literal[1]) -> str: ...
285
+ @overload
286
+ def date_of_birth(self, count: int) -> str | list[str]: ...
287
+ def date_of_birth(
288
+ self,
289
+ count: int = 1,
290
+ min_age: int = 18,
291
+ max_age: int = 80,
292
+ fmt: str = "%Y-%m-%d",
293
+ ) -> str | list[str]:
294
+ """Generate a random date of birth.
295
+
296
+ Parameters
297
+ ----------
298
+ count : int
299
+ Number of dates to generate.
300
+ min_age : int
301
+ Minimum age in years.
302
+ max_age : int
303
+ Maximum age in years.
304
+ fmt : str
305
+ strftime format string.
306
+ """
307
+ # Compute today() once for the entire batch
308
+ today = _dt.date.today()
309
+ start = today.replace(year=today.year - max_age)
310
+ end = today.replace(year=today.year - min_age)
311
+
312
+ if fmt == "%Y-%m-%d":
313
+ _iso = self._date_to_iso
314
+ s_ord, e_ord = start.toordinal(), end.toordinal()
315
+ _ri = self._engine.random_int
316
+ _from_ord = _dt.date.fromordinal
317
+ if count == 1:
318
+ return _iso(_from_ord(_ri(s_ord, e_ord)))
319
+ return [_iso(_from_ord(_ri(s_ord, e_ord))) for _ in range(count)]
320
+ if count == 1:
321
+ return self._one_date(start, end).strftime(fmt)
322
+ return [self._one_date(start, end).strftime(fmt) for _ in range(count)]
323
+
324
+ def date_object(self, count: int = 1) -> _dt.date | list[_dt.date]:
325
+ """Generate a random ``datetime.date`` object.
326
+
327
+ Parameters
328
+ ----------
329
+ count : int
330
+ Number of date objects to generate.
331
+ """
332
+ if count == 1:
333
+ return self._one_date()
334
+ return [self._one_date() for _ in range(count)]
335
+
336
+ def datetime_object(self, count: int = 1) -> _dt.datetime | list[_dt.datetime]:
337
+ """Generate a random ``datetime.datetime`` object.
338
+
339
+ Parameters
340
+ ----------
341
+ count : int
342
+ Number of datetime objects to generate.
343
+ """
344
+ if count == 1:
345
+ return self._one_datetime()
346
+ return [self._one_datetime() for _ in range(count)]
347
+
348
+ @overload
349
+ def timezone(self) -> str: ...
350
+ @overload
351
+ def timezone(self, count: Literal[1]) -> str: ...
352
+ @overload
353
+ def timezone(self, count: int) -> str | list[str]: ...
354
+ def timezone(self, count: int = 1) -> str | list[str]:
355
+ """Generate a random IANA timezone string (e.g. ``"Europe/Berlin"``).
356
+
357
+ Parameters
358
+ ----------
359
+ count : int
360
+ Number of timezone strings to generate.
361
+ """
362
+ if count == 1:
363
+ return self._engine.choice(_TIMEZONES)
364
+ return self._engine.choices(_TIMEZONES, count)
365
+
366
+ def unix_timestamp(
367
+ self,
368
+ count: int = 1,
369
+ start: _dt.date | None = None,
370
+ end: _dt.date | None = None,
371
+ ) -> int | list[int]:
372
+ """Generate a random Unix timestamp (seconds since epoch).
373
+
374
+ Parameters
375
+ ----------
376
+ count : int
377
+ Number of timestamps to generate.
378
+ start : datetime.date | None
379
+ Earliest date (default: 1970-01-01).
380
+ end : datetime.date | None
381
+ Latest date (default: 2030-12-31).
382
+ """
383
+ # Use pre-computed constants for default range to avoid
384
+ # .toordinal() per call.
385
+ if start is None and end is None:
386
+ min_ts = _MIN_TIMESTAMP
387
+ max_ts = _MAX_TIMESTAMP
388
+ else:
389
+ s = start or _MIN_DATE
390
+ e = end or _MAX_DATE
391
+ min_ts = (s.toordinal() - _EPOCH_ORDINAL) * _SECONDS_IN_DAY
392
+ max_ts = (e.toordinal() - _EPOCH_ORDINAL + 1) * _SECONDS_IN_DAY - 1
393
+
394
+ if count == 1:
395
+ return self._engine.random_int(min_ts, max_ts)
396
+ _ri = self._engine.random_int
397
+ return [_ri(min_ts, max_ts) for _ in range(count)]
@@ -0,0 +1,316 @@
1
+ """E-commerce provider — products, SKUs, tracking, reviews."""
2
+
3
+ from typing import Literal, overload
4
+
5
+ from dataforge.providers.base import BaseProvider
6
+
7
+ _PRODUCT_ADJECTIVES: tuple[str, ...] = (
8
+ "Premium",
9
+ "Deluxe",
10
+ "Ultra",
11
+ "Pro",
12
+ "Essential",
13
+ "Classic",
14
+ "Modern",
15
+ "Smart",
16
+ "Eco",
17
+ "Advanced",
18
+ "Elite",
19
+ "Basic",
20
+ "Compact",
21
+ "Portable",
22
+ "Wireless",
23
+ "Digital",
24
+ "Organic",
25
+ "Vintage",
26
+ "Artisan",
27
+ "Custom",
28
+ "Heavy-Duty",
29
+ "Lightweight",
30
+ "Industrial",
31
+ "Professional",
32
+ "Commercial",
33
+ "Residential",
34
+ )
35
+
36
+ _PRODUCT_MATERIALS: tuple[str, ...] = (
37
+ "Steel",
38
+ "Aluminum",
39
+ "Bamboo",
40
+ "Cotton",
41
+ "Leather",
42
+ "Silk",
43
+ "Wooden",
44
+ "Ceramic",
45
+ "Glass",
46
+ "Rubber",
47
+ "Plastic",
48
+ "Granite",
49
+ "Marble",
50
+ "Carbon Fiber",
51
+ "Titanium",
52
+ "Bronze",
53
+ "Copper",
54
+ "Linen",
55
+ "Wool",
56
+ "Concrete",
57
+ "Paper",
58
+ "Foam",
59
+ "Nylon",
60
+ )
61
+
62
+ _PRODUCT_ITEMS: tuple[str, ...] = (
63
+ "Chair",
64
+ "Table",
65
+ "Lamp",
66
+ "Keyboard",
67
+ "Mouse",
68
+ "Monitor",
69
+ "Headphones",
70
+ "Speaker",
71
+ "Camera",
72
+ "Watch",
73
+ "Bag",
74
+ "Wallet",
75
+ "Bottle",
76
+ "Mug",
77
+ "Plate",
78
+ "Bowl",
79
+ "Knife",
80
+ "Pan",
81
+ "Pillow",
82
+ "Blanket",
83
+ "Towel",
84
+ "Mirror",
85
+ "Clock",
86
+ "Frame",
87
+ "Shelf",
88
+ "Desk",
89
+ "Sofa",
90
+ "Bench",
91
+ "Stool",
92
+ "Rack",
93
+ "Cabinet",
94
+ "Drawer",
95
+ "Basket",
96
+ "Box",
97
+ "Case",
98
+ "Cover",
99
+ "Mat",
100
+ "Rug",
101
+ "Curtain",
102
+ "Vase",
103
+ "Candle",
104
+ "Planter",
105
+ "Tray",
106
+ "Hook",
107
+ "Stand",
108
+ )
109
+
110
+ _PRODUCT_CATEGORIES: tuple[str, ...] = (
111
+ "Electronics",
112
+ "Clothing",
113
+ "Home & Garden",
114
+ "Sports & Outdoors",
115
+ "Books",
116
+ "Toys & Games",
117
+ "Automotive",
118
+ "Health & Beauty",
119
+ "Food & Beverages",
120
+ "Office Supplies",
121
+ "Pet Supplies",
122
+ "Jewelry",
123
+ "Music",
124
+ "Tools & Hardware",
125
+ "Baby Products",
126
+ "Arts & Crafts",
127
+ "Industrial",
128
+ "Software",
129
+ "Furniture",
130
+ )
131
+
132
+ _TRACKING_PREFIXES: tuple[str, ...] = (
133
+ "1Z",
134
+ "94",
135
+ "92",
136
+ "TBA",
137
+ "JD",
138
+ "SF",
139
+ "YT",
140
+ )
141
+
142
+ _REVIEW_TITLES: tuple[str, ...] = (
143
+ "Great product!",
144
+ "Highly recommended",
145
+ "Good value for money",
146
+ "Exceeded expectations",
147
+ "Exactly as described",
148
+ "Decent quality",
149
+ "Not bad",
150
+ "Could be better",
151
+ "Disappointed",
152
+ "Amazing!",
153
+ "Perfect fit",
154
+ "Solid build quality",
155
+ "Love it!",
156
+ "Just okay",
157
+ "Works as expected",
158
+ "Would buy again",
159
+ "Five stars",
160
+ "Better than expected",
161
+ "Fantastic purchase",
162
+ "Very satisfied",
163
+ )
164
+
165
+
166
+ class EcommerceProvider(BaseProvider):
167
+ """Generates fake e-commerce data."""
168
+
169
+ __slots__ = ()
170
+
171
+ _provider_name = "ecommerce"
172
+ _locale_modules: tuple[str, ...] = ()
173
+ _field_map: dict[str, str] = {
174
+ "product_name": "product_name",
175
+ "product": "product_name",
176
+ "product_category": "product_category",
177
+ "category": "product_category",
178
+ "sku": "sku",
179
+ "price_with_currency": "price_with_currency",
180
+ "review_rating": "review_rating",
181
+ "rating": "review_rating",
182
+ "review_title": "review_title",
183
+ "tracking_number": "tracking_number",
184
+ "order_id": "order_id",
185
+ }
186
+
187
+ _CURRENCIES: tuple[tuple[str, str], ...] = (
188
+ ("$", "USD"),
189
+ ("€", "EUR"),
190
+ ("£", "GBP"),
191
+ ("¥", "JPY"),
192
+ ("$", "CAD"),
193
+ ("$", "AUD"),
194
+ )
195
+
196
+ # --- Scalar helpers ---
197
+
198
+ def _one_product_name(self) -> str:
199
+ _c = self._engine.choice
200
+ return (
201
+ f"{_c(_PRODUCT_ADJECTIVES)} {_c(_PRODUCT_MATERIALS)} {_c(_PRODUCT_ITEMS)}"
202
+ )
203
+
204
+ def _one_sku(self) -> str:
205
+ letters = "".join(chr(self._engine.random_int(65, 90)) for _ in range(3))
206
+ return f"{letters}-{self._engine.random_digits_str(6)}"
207
+
208
+ def _one_tracking(self) -> str:
209
+ prefix = self._engine.choice(_TRACKING_PREFIXES)
210
+ return prefix + self._engine.random_digits_str(18)
211
+
212
+ def _one_order_id(self) -> str:
213
+ return f"ORD-{self._engine.random_digits_str(10)}"
214
+
215
+ # --- Public API ---
216
+
217
+ @overload
218
+ def product_name(self) -> str: ...
219
+ @overload
220
+ def product_name(self, count: Literal[1]) -> str: ...
221
+ @overload
222
+ def product_name(self, count: int) -> str | list[str]: ...
223
+ def product_name(self, count: int = 1) -> str | list[str]:
224
+ """Generate a fake product name."""
225
+ if count == 1:
226
+ return self._one_product_name()
227
+ return [self._one_product_name() for _ in range(count)]
228
+
229
+ @overload
230
+ def product_category(self) -> str: ...
231
+ @overload
232
+ def product_category(self, count: Literal[1]) -> str: ...
233
+ @overload
234
+ def product_category(self, count: int) -> str | list[str]: ...
235
+ def product_category(self, count: int = 1) -> str | list[str]:
236
+ """Generate a product category."""
237
+ if count == 1:
238
+ return self._engine.choice(_PRODUCT_CATEGORIES)
239
+ return self._engine.choices(_PRODUCT_CATEGORIES, count)
240
+
241
+ @overload
242
+ def sku(self) -> str: ...
243
+ @overload
244
+ def sku(self, count: Literal[1]) -> str: ...
245
+ @overload
246
+ def sku(self, count: int) -> str | list[str]: ...
247
+ def sku(self, count: int = 1) -> str | list[str]:
248
+ """Generate a product SKU (e.g., ABC-123456)."""
249
+ if count == 1:
250
+ return self._one_sku()
251
+ return [self._one_sku() for _ in range(count)]
252
+
253
+ @overload
254
+ def price_with_currency(self) -> str: ...
255
+ @overload
256
+ def price_with_currency(self, count: Literal[1]) -> str: ...
257
+ @overload
258
+ def price_with_currency(self, count: int) -> str | list[str]: ...
259
+ def price_with_currency(self, count: int = 1) -> str | list[str]:
260
+ """Generate a price with currency symbol (e.g., $49.99)."""
261
+ if count == 1:
262
+ sym, _ = self._engine.choice(self._CURRENCIES)
263
+ return f"{sym}{self._engine.random_int(1, 99999) / 100:.2f}"
264
+ _ri = self._engine.random_int
265
+ _c = self._engine.choice
266
+ return [
267
+ f"{_c(self._CURRENCIES)[0]}{_ri(1, 99999) / 100:.2f}" for _ in range(count)
268
+ ]
269
+
270
+ @overload
271
+ def review_rating(self) -> int: ...
272
+ @overload
273
+ def review_rating(self, count: Literal[1]) -> int: ...
274
+ @overload
275
+ def review_rating(self, count: int) -> int | list[int]: ...
276
+ def review_rating(self, count: int = 1) -> int | list[int]:
277
+ """Generate a review rating (1-5)."""
278
+ if count == 1:
279
+ return self._engine.random_int(1, 5)
280
+ return [self._engine.random_int(1, 5) for _ in range(count)]
281
+
282
+ @overload
283
+ def review_title(self) -> str: ...
284
+ @overload
285
+ def review_title(self, count: Literal[1]) -> str: ...
286
+ @overload
287
+ def review_title(self, count: int) -> str | list[str]: ...
288
+ def review_title(self, count: int = 1) -> str | list[str]:
289
+ """Generate a product review title."""
290
+ if count == 1:
291
+ return self._engine.choice(_REVIEW_TITLES)
292
+ return self._engine.choices(_REVIEW_TITLES, count)
293
+
294
+ @overload
295
+ def tracking_number(self) -> str: ...
296
+ @overload
297
+ def tracking_number(self, count: Literal[1]) -> str: ...
298
+ @overload
299
+ def tracking_number(self, count: int) -> str | list[str]: ...
300
+ def tracking_number(self, count: int = 1) -> str | list[str]:
301
+ """Generate a shipping tracking number."""
302
+ if count == 1:
303
+ return self._one_tracking()
304
+ return [self._one_tracking() for _ in range(count)]
305
+
306
+ @overload
307
+ def order_id(self) -> str: ...
308
+ @overload
309
+ def order_id(self, count: Literal[1]) -> str: ...
310
+ @overload
311
+ def order_id(self, count: int) -> str | list[str]: ...
312
+ def order_id(self, count: int = 1) -> str | list[str]:
313
+ """Generate an order ID (e.g., ORD-1234567890)."""
314
+ if count == 1:
315
+ return self._one_order_id()
316
+ return [self._one_order_id() for _ in range(count)]