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.
- dataforge/__init__.py +20 -0
- dataforge/backend.py +147 -0
- dataforge/cli.py +166 -0
- dataforge/core.py +1169 -0
- dataforge/locales/__init__.py +1 -0
- dataforge/locales/ar_SA/__init__.py +1 -0
- dataforge/locales/ar_SA/address.py +128 -0
- dataforge/locales/ar_SA/company.py +183 -0
- dataforge/locales/ar_SA/internet.py +25 -0
- dataforge/locales/ar_SA/person.py +217 -0
- dataforge/locales/ar_SA/phone.py +15 -0
- dataforge/locales/de_DE/__init__.py +1 -0
- dataforge/locales/de_DE/address.py +148 -0
- dataforge/locales/de_DE/company.py +125 -0
- dataforge/locales/de_DE/internet.py +32 -0
- dataforge/locales/de_DE/person.py +212 -0
- dataforge/locales/de_DE/phone.py +17 -0
- dataforge/locales/en_AU/__init__.py +1 -0
- dataforge/locales/en_AU/address.py +231 -0
- dataforge/locales/en_AU/company.py +193 -0
- dataforge/locales/en_AU/internet.py +34 -0
- dataforge/locales/en_AU/person.py +370 -0
- dataforge/locales/en_AU/phone.py +16 -0
- dataforge/locales/en_CA/__init__.py +1 -0
- dataforge/locales/en_CA/address.py +276 -0
- dataforge/locales/en_CA/company.py +193 -0
- dataforge/locales/en_CA/internet.py +34 -0
- dataforge/locales/en_CA/person.py +377 -0
- dataforge/locales/en_CA/phone.py +15 -0
- dataforge/locales/en_GB/__init__.py +1 -0
- dataforge/locales/en_GB/address.py +312 -0
- dataforge/locales/en_GB/company.py +196 -0
- dataforge/locales/en_GB/internet.py +34 -0
- dataforge/locales/en_GB/person.py +372 -0
- dataforge/locales/en_GB/phone.py +15 -0
- dataforge/locales/en_US/__init__.py +1 -0
- dataforge/locales/en_US/address.py +268 -0
- dataforge/locales/en_US/company.py +191 -0
- dataforge/locales/en_US/internet.py +34 -0
- dataforge/locales/en_US/person.py +370 -0
- dataforge/locales/en_US/phone.py +15 -0
- dataforge/locales/es_ES/__init__.py +1 -0
- dataforge/locales/es_ES/address.py +151 -0
- dataforge/locales/es_ES/company.py +125 -0
- dataforge/locales/es_ES/internet.py +30 -0
- dataforge/locales/es_ES/person.py +207 -0
- dataforge/locales/es_ES/phone.py +15 -0
- dataforge/locales/fr_FR/__init__.py +1 -0
- dataforge/locales/fr_FR/address.py +145 -0
- dataforge/locales/fr_FR/company.py +125 -0
- dataforge/locales/fr_FR/internet.py +30 -0
- dataforge/locales/fr_FR/person.py +212 -0
- dataforge/locales/fr_FR/phone.py +15 -0
- dataforge/locales/hi_IN/__init__.py +1 -0
- dataforge/locales/hi_IN/address.py +177 -0
- dataforge/locales/hi_IN/company.py +191 -0
- dataforge/locales/hi_IN/internet.py +26 -0
- dataforge/locales/hi_IN/person.py +218 -0
- dataforge/locales/hi_IN/phone.py +21 -0
- dataforge/locales/it_IT/__init__.py +1 -0
- dataforge/locales/it_IT/address.py +218 -0
- dataforge/locales/it_IT/company.py +151 -0
- dataforge/locales/it_IT/internet.py +31 -0
- dataforge/locales/it_IT/person.py +187 -0
- dataforge/locales/it_IT/phone.py +15 -0
- dataforge/locales/ja_JP/__init__.py +1 -0
- dataforge/locales/ja_JP/address.py +174 -0
- dataforge/locales/ja_JP/company.py +121 -0
- dataforge/locales/ja_JP/internet.py +30 -0
- dataforge/locales/ja_JP/person.py +207 -0
- dataforge/locales/ja_JP/phone.py +18 -0
- dataforge/locales/ko_KR/__init__.py +1 -0
- dataforge/locales/ko_KR/address.py +121 -0
- dataforge/locales/ko_KR/company.py +151 -0
- dataforge/locales/ko_KR/internet.py +30 -0
- dataforge/locales/ko_KR/person.py +157 -0
- dataforge/locales/ko_KR/phone.py +26 -0
- dataforge/locales/nl_NL/__init__.py +1 -0
- dataforge/locales/nl_NL/address.py +152 -0
- dataforge/locales/nl_NL/company.py +182 -0
- dataforge/locales/nl_NL/internet.py +41 -0
- dataforge/locales/nl_NL/person.py +218 -0
- dataforge/locales/nl_NL/phone.py +19 -0
- dataforge/locales/pl_PL/__init__.py +1 -0
- dataforge/locales/pl_PL/address.py +140 -0
- dataforge/locales/pl_PL/company.py +183 -0
- dataforge/locales/pl_PL/internet.py +36 -0
- dataforge/locales/pl_PL/person.py +217 -0
- dataforge/locales/pl_PL/phone.py +15 -0
- dataforge/locales/pt_BR/__init__.py +1 -0
- dataforge/locales/pt_BR/address.py +127 -0
- dataforge/locales/pt_BR/company.py +151 -0
- dataforge/locales/pt_BR/internet.py +31 -0
- dataforge/locales/pt_BR/person.py +187 -0
- dataforge/locales/pt_BR/phone.py +15 -0
- dataforge/locales/ru_RU/__init__.py +1 -0
- dataforge/locales/ru_RU/address.py +156 -0
- dataforge/locales/ru_RU/company.py +168 -0
- dataforge/locales/ru_RU/internet.py +26 -0
- dataforge/locales/ru_RU/person.py +218 -0
- dataforge/locales/ru_RU/phone.py +16 -0
- dataforge/locales/zh_CN/__init__.py +1 -0
- dataforge/locales/zh_CN/address.py +141 -0
- dataforge/locales/zh_CN/company.py +151 -0
- dataforge/locales/zh_CN/internet.py +30 -0
- dataforge/locales/zh_CN/person.py +157 -0
- dataforge/locales/zh_CN/phone.py +25 -0
- dataforge/providers/__init__.py +1 -0
- dataforge/providers/address.py +460 -0
- dataforge/providers/ai_chat.py +170 -0
- dataforge/providers/ai_prompt.py +447 -0
- dataforge/providers/automotive.py +416 -0
- dataforge/providers/barcode.py +149 -0
- dataforge/providers/base.py +34 -0
- dataforge/providers/color.py +247 -0
- dataforge/providers/company.py +144 -0
- dataforge/providers/crypto.py +105 -0
- dataforge/providers/datetime.py +397 -0
- dataforge/providers/ecommerce.py +316 -0
- dataforge/providers/education.py +234 -0
- dataforge/providers/file.py +271 -0
- dataforge/providers/finance.py +545 -0
- dataforge/providers/geo.py +332 -0
- dataforge/providers/government.py +114 -0
- dataforge/providers/internet.py +351 -0
- dataforge/providers/llm.py +726 -0
- dataforge/providers/lorem.py +241 -0
- dataforge/providers/medical.py +364 -0
- dataforge/providers/misc.py +196 -0
- dataforge/providers/network.py +283 -0
- dataforge/providers/payment.py +300 -0
- dataforge/providers/person.py +195 -0
- dataforge/providers/phone.py +87 -0
- dataforge/providers/profile.py +265 -0
- dataforge/providers/science.py +365 -0
- dataforge/providers/text.py +365 -0
- dataforge/py.typed +0 -0
- dataforge/pytest_plugin.py +80 -0
- dataforge/registry.py +164 -0
- dataforge/schema.py +772 -0
- dataforge/unique.py +171 -0
- dataforge_py-0.2.0.dist-info/METADATA +964 -0
- dataforge_py-0.2.0.dist-info/RECORD +145 -0
- dataforge_py-0.2.0.dist-info/WHEEL +4 -0
- 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)]
|