persian-devkit 1.0.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 (51) hide show
  1. persian_devkit/__init__.py +4 -0
  2. persian_devkit/__main__.py +5 -0
  3. persian_devkit/cli.py +55 -0
  4. persian_devkit/commands/__init__.py +16 -0
  5. persian_devkit/commands/base64_cmd.py +51 -0
  6. persian_devkit/commands/color_cmd.py +143 -0
  7. persian_devkit/commands/csv_cmd.py +121 -0
  8. persian_devkit/commands/date_cmd.py +98 -0
  9. persian_devkit/commands/env_cmd.py +100 -0
  10. persian_devkit/commands/gitignore_cmd.py +68 -0
  11. persian_devkit/commands/hash_cmd.py +71 -0
  12. persian_devkit/commands/image_cmd.py +88 -0
  13. persian_devkit/commands/jq_cmd.py +92 -0
  14. persian_devkit/commands/json_cmd.py +135 -0
  15. persian_devkit/commands/license_cmd.py +67 -0
  16. persian_devkit/commands/lorem_cmd.py +53 -0
  17. persian_devkit/commands/name_cmd.py +98 -0
  18. persian_devkit/commands/number_cmd.py +65 -0
  19. persian_devkit/commands/password_cmd.py +55 -0
  20. persian_devkit/commands/qrcode_cmd.py +46 -0
  21. persian_devkit/commands/random_cmd.py +110 -0
  22. persian_devkit/commands/scaffold_cmd.py +155 -0
  23. persian_devkit/commands/template_cmd.py +87 -0
  24. persian_devkit/commands/text_cmd.py +157 -0
  25. persian_devkit/commands/time_cmd.py +96 -0
  26. persian_devkit/commands/toml_cmd.py +85 -0
  27. persian_devkit/commands/url_cmd.py +102 -0
  28. persian_devkit/commands/uuid_cmd.py +45 -0
  29. persian_devkit/commands/yaml_cmd.py +94 -0
  30. persian_devkit/main.py +63 -0
  31. persian_devkit/py.typed +0 -0
  32. persian_devkit/utils/__init__.py +1 -0
  33. persian_devkit/utils/color_utils.py +112 -0
  34. persian_devkit/utils/crypto_utils.py +157 -0
  35. persian_devkit/utils/data_utils.py +146 -0
  36. persian_devkit/utils/date_utils.py +58 -0
  37. persian_devkit/utils/fake_utils.py +144 -0
  38. persian_devkit/utils/gitignore_data.py +211 -0
  39. persian_devkit/utils/image_utils.py +37 -0
  40. persian_devkit/utils/jsonpath_utils.py +94 -0
  41. persian_devkit/utils/license_data.py +190 -0
  42. persian_devkit/utils/number_utils.py +78 -0
  43. persian_devkit/utils/qrcode_utils.py +84 -0
  44. persian_devkit/utils/text_utils.py +166 -0
  45. persian_devkit/utils/time_utils.py +81 -0
  46. persian_devkit/utils/url_utils.py +54 -0
  47. persian_devkit-1.0.0.dist-info/METADATA +702 -0
  48. persian_devkit-1.0.0.dist-info/RECORD +51 -0
  49. persian_devkit-1.0.0.dist-info/WHEEL +4 -0
  50. persian_devkit-1.0.0.dist-info/entry_points.txt +2 -0
  51. persian_devkit-1.0.0.dist-info/licenses/LICENSE.txt +21 -0
@@ -0,0 +1,157 @@
1
+ """توابع کمکی کریپتوگرافی، هش، رمز و base64."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import hashlib
6
+ import math
7
+ import secrets
8
+ import string
9
+ from pathlib import Path
10
+
11
+ HASH_ALGOS = ("md5", "sha1", "sha256", "sha512", "blake2b", "blake2s")
12
+
13
+ # مجموعه‌های کاراکتری برای تولید رمز
14
+ PASSWORD_SETS: dict[str, str] = {
15
+ "lower": string.ascii_lowercase,
16
+ "upper": string.ascii_uppercase,
17
+ "digits": string.digits,
18
+ "symbols": "!@#$%^&*()-_=+[]{};:,.<>?/",
19
+ }
20
+
21
+ # الفبای Nano ID (URL-safe)
22
+ _NANOID_ALPHABET = string.ascii_letters + string.digits + "_-"
23
+
24
+
25
+ #hash
26
+
27
+
28
+ def hash_text(text: str, algo: str = "sha256") -> str:
29
+ """هش یک رشتهٔ متنی را برمی‌گرداند."""
30
+ if algo not in HASH_ALGOS:
31
+ raise ValueError(f"الگوریتم پشتیبانی‌نشده: {algo}")
32
+ h = hashlib.new(algo)
33
+ h.update(text.encode("utf-8"))
34
+ return h.hexdigest()
35
+
36
+
37
+ def hash_file(path: Path, algo: str = "sha256", chunk: int = 1 << 20) -> str:
38
+ """هش یک فایل را به‌صورت chunk-by-chunk حساب می‌کند (مناسب فایل‌های بزرگ)."""
39
+ if algo not in HASH_ALGOS:
40
+ raise ValueError(f"الگوریتم پشتیبانی‌نشده: {algo}")
41
+ h = hashlib.new(algo)
42
+ with path.open("rb") as f:
43
+ while True:
44
+ block = f.read(chunk)
45
+ if not block:
46
+ break
47
+ h.update(block)
48
+ return h.hexdigest()
49
+
50
+
51
+ #base64
52
+
53
+
54
+ def b64_encode(text: str, url_safe: bool = False) -> str:
55
+ """رمزگذاری Base64 با پشتیبانی UTF-8."""
56
+ raw = text.encode("utf-8")
57
+ if url_safe:
58
+ return base64.urlsafe_b64encode(raw).decode("ascii")
59
+ return base64.b64encode(raw).decode("ascii")
60
+
61
+
62
+ def b64_decode(text: str, url_safe: bool = False) -> str:
63
+ """رمزگشایی Base64 به UTF-8."""
64
+ cleaned = text.strip().encode("ascii")
65
+ try:
66
+ if url_safe:
67
+ raw = base64.urlsafe_b64decode(cleaned)
68
+ else:
69
+ raw = base64.b64decode(cleaned, validate=True)
70
+ except Exception as exc:
71
+ raise ValueError(f"Base64 نامعتبر: {exc}") from exc
72
+ return raw.decode("utf-8")
73
+
74
+
75
+ #password
76
+
77
+
78
+ def generate_password(
79
+ length: int = 16,
80
+ use_lower: bool = True,
81
+ use_upper: bool = True,
82
+ use_digits: bool = True,
83
+ use_symbols: bool = True,
84
+ ) -> str:
85
+ """تولید رمز عبور امن با مجموعه‌های کاراکتری انتخابی."""
86
+ pools: list[str] = []
87
+ if use_lower:
88
+ pools.append(PASSWORD_SETS["lower"])
89
+ if use_upper:
90
+ pools.append(PASSWORD_SETS["upper"])
91
+ if use_digits:
92
+ pools.append(PASSWORD_SETS["digits"])
93
+ if use_symbols:
94
+ pools.append(PASSWORD_SETS["symbols"])
95
+
96
+ if not pools:
97
+ raise ValueError("حداقل یک مجموعهٔ کاراکتری باید فعال باشد.")
98
+ if length < len(pools):
99
+ raise ValueError(
100
+ f"طول رمز باید حداقل {len(pools)} باشد تا از هر مجموعه یک کاراکتر بیاید."
101
+ )
102
+
103
+ # تضمین حداقل یک کاراکتر از هر مجموعه
104
+ chars = [secrets.choice(pool) for pool in pools]
105
+ all_chars = "".join(pools)
106
+ chars.extend(secrets.choice(all_chars) for _ in range(length - len(pools)))
107
+ # Fisher-Yates shuffle امن
108
+ for i in range(len(chars) - 1, 0, -1):
109
+ j = secrets.randbelow(i + 1)
110
+ chars[i], chars[j] = chars[j], chars[i]
111
+ return "".join(chars)
112
+
113
+
114
+ def password_strength(password: str) -> dict:
115
+ """امتیاز قدرت رمز عبور (۰ تا ۱۰۰) و برچسب."""
116
+ if not password:
117
+ return {"score": 0, "label": "خالی", "entropy": 0.0}
118
+
119
+ pools = 0
120
+ if any(c.islower() for c in password):
121
+ pools += 26
122
+ if any(c.isupper() for c in password):
123
+ pools += 26
124
+ if any(c.isdigit() for c in password):
125
+ pools += 10
126
+ if any(c in PASSWORD_SETS["symbols"] for c in password):
127
+ pools += len(PASSWORD_SETS["symbols"])
128
+
129
+ if pools == 0:
130
+ entropy = 0.0
131
+ else:
132
+ entropy = len(password) * math.log2(pools)
133
+
134
+ # نگاشت آنتروپی به امتیاز ۰-۱۰۰
135
+ # < 28 → خیلی ضعیف، 28-35 ضعیف، 36-59 متوسط، 60-127 قوی، 128+ خیلی قوی
136
+ if entropy < 28:
137
+ score, label = 15, "خیلی ضعیف"
138
+ elif entropy < 36:
139
+ score, label = 30, "ضعیف"
140
+ elif entropy < 60:
141
+ score, label = 55, "متوسط"
142
+ elif entropy < 128:
143
+ score, label = 80, "قوی"
144
+ else:
145
+ score, label = 100, "خیلی قوی"
146
+
147
+ return {"score": score, "label": label, "entropy": round(entropy, 1)}
148
+
149
+
150
+ #nanoid
151
+
152
+
153
+ def nanoid(length: int = 21) -> str:
154
+ """تولید شناسهٔ کوتاه Nano ID با الفبای URL-safe."""
155
+ if length < 1:
156
+ raise ValueError("طول باید حداقل ۱ باشد.")
157
+ return "".join(secrets.choice(_NANOID_ALPHABET) for _ in range(length))
@@ -0,0 +1,146 @@
1
+ """توابع کمکی مشترک برای YAML، TOML، CSV و ENV."""
2
+ from __future__ import annotations
3
+
4
+ import csv
5
+ import io
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, Optional
10
+
11
+ if sys.version_info >= (3, 11):
12
+ import tomllib
13
+ else: # pragma: no cover
14
+ import tomli as tomllib
15
+
16
+ import tomli_w
17
+ import yaml
18
+
19
+
20
+ #IO
21
+
22
+
23
+ def read_text(path: Path) -> str:
24
+ """فایل متنی را با UTF-8 می‌خواند."""
25
+ return path.read_text(encoding="utf-8")
26
+
27
+
28
+ def write_text(path: Path, text: str) -> None:
29
+ """نوشتن متن در فایل با UTF-8."""
30
+ path.write_text(text, encoding="utf-8")
31
+
32
+
33
+ #YAML
34
+
35
+
36
+ def parse_yaml(text: str) -> Any:
37
+ """پارس YAML به شیء پایتون."""
38
+ try:
39
+ return yaml.safe_load(text)
40
+ except yaml.YAMLError as exc:
41
+ raise ValueError(f"YAML نامعتبر: {exc}") from exc
42
+
43
+
44
+ def dump_yaml(data: Any) -> str:
45
+ """تبدیل شیء پایتون به YAML."""
46
+ return yaml.safe_dump(
47
+ data, allow_unicode=True, sort_keys=False, default_flow_style=False
48
+ )
49
+
50
+
51
+ #TOML
52
+
53
+
54
+ def parse_toml(text: str) -> dict:
55
+ """پارس TOML به دیکشنری."""
56
+ try:
57
+ return tomllib.loads(text)
58
+ except tomllib.TOMLDecodeError as exc:
59
+ raise ValueError(f"TOML نامعتبر: {exc}") from exc
60
+
61
+
62
+ def dump_toml(data: dict) -> str:
63
+ """تبدیل دیکشنری به TOML."""
64
+ try:
65
+ return tomli_w.dumps(data)
66
+ except (TypeError, ValueError) as exc:
67
+ raise ValueError(f"خطا در تولید TOML: {exc}") from exc
68
+
69
+
70
+ #JSON
71
+
72
+
73
+ def parse_json(text: str) -> Any:
74
+ """پارس JSON با پیام خطای فارسی."""
75
+ try:
76
+ return json.loads(text)
77
+ except json.JSONDecodeError as exc:
78
+ raise ValueError(f"JSON نامعتبر: {exc}") from exc
79
+
80
+
81
+ def dump_json(data: Any, indent: int = 2, ensure_ascii: bool = False) -> str:
82
+ """تبدیل شیء به JSON با پشتیبانی یونیکد."""
83
+ return json.dumps(data, ensure_ascii=ensure_ascii, indent=indent)
84
+
85
+
86
+ #CSV
87
+
88
+
89
+ def read_csv(path: Path, delimiter: str = ",") -> tuple[list[str], list[dict]]:
90
+ """فایل CSV را می‌خواند و (هدر، ردیف‌ها) برمی‌گرداند.
91
+
92
+ هر ردیف یک دیکشنری با کلیدهای هدر است.
93
+ """
94
+ text = read_text(path)
95
+ reader = csv.DictReader(io.StringIO(text), delimiter=delimiter)
96
+ if reader.fieldnames is None:
97
+ return [], []
98
+ headers = list(reader.fieldnames)
99
+ rows = [dict(r) for r in reader]
100
+ return headers, rows
101
+
102
+
103
+ def write_csv(path: Path, headers: list[str], rows: list[dict], delimiter: str = ",") -> None:
104
+ """نوشتن ردیف‌ها در فایل CSV."""
105
+ buf = io.StringIO()
106
+ writer = csv.DictWriter(buf, fieldnames=headers, delimiter=delimiter)
107
+ writer.writeheader()
108
+ for row in rows:
109
+ writer.writerow(row)
110
+ write_text(path, buf.getvalue())
111
+
112
+
113
+ #ENV
114
+
115
+
116
+ def parse_env(text: str) -> dict[str, str]:
117
+ """پارس محتوای .env به دیکشنری.
118
+
119
+ خطوط با # به‌عنوان کامنت نادیده گرفته می‌شوند.
120
+ مقادیر داخل "..." یا '...' بدون کوتیشن برمی‌گردند.
121
+ """
122
+ result: dict[str, str] = {}
123
+ for raw_line in text.splitlines():
124
+ line = raw_line.strip()
125
+ if not line or line.startswith("#"):
126
+ continue
127
+ if "=" not in line:
128
+ continue
129
+ key, _, value = line.partition("=")
130
+ key = key.strip()
131
+ value = value.strip()
132
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
133
+ value = value[1:-1]
134
+ if key:
135
+ result[key] = value
136
+ return result
137
+
138
+
139
+ def dump_env(data: dict[str, str]) -> str:
140
+ """تبدیل دیکشنری به محتوای .env."""
141
+ lines: list[str] = []
142
+ for key, value in data.items():
143
+ if any(c in value for c in (" ", "#", "'", '"')):
144
+ value = f'"{value}"'
145
+ lines.append(f"{key}={value}")
146
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,58 @@
1
+ """توابع کمکی تبدیل و محاسبهٔ تاریخ."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import date, datetime
5
+
6
+ import jdatetime
7
+
8
+
9
+ class DateParseError(ValueError):
10
+ """خطا در تجزیهٔ تاریخ."""
11
+
12
+
13
+ def parse_gregorian(value: str) -> date:
14
+ """تاریخ میلادی را از رشتهٔ YYYY-MM-DD می‌خواند."""
15
+ try:
16
+ return datetime.strptime(value.strip(), "%Y-%m-%d").date()
17
+ except (ValueError, AttributeError) as exc:
18
+ raise DateParseError(f"تاریخ میلادی نامعتبر: {value}") from exc
19
+
20
+
21
+ def parse_jalali(value: str) -> jdatetime.date:
22
+ """تاریخ شمسی را از رشتهٔ YYYY/MM/DD یا YYYY-MM-DD می‌خواند."""
23
+ if not isinstance(value, str):
24
+ raise DateParseError(f"تاریخ شمسی نامعتبر: {value}")
25
+ normalized = value.strip().replace("-", "/")
26
+ parts = normalized.split("/")
27
+ if len(parts) != 3:
28
+ raise DateParseError(f"تاریخ شمسی نامعتبر: {value}")
29
+ try:
30
+ y, m, d = (int(p) for p in parts)
31
+ return jdatetime.date(y, m, d)
32
+ except (ValueError, TypeError) as exc:
33
+ raise DateParseError(f"تاریخ شمسی نامعتبر: {value}") from exc
34
+
35
+
36
+ def gregorian_to_jalali(g: date) -> jdatetime.date:
37
+ """تبدیل تاریخ میلادی به شمسی."""
38
+ return jdatetime.date.fromgregorian(date=g)
39
+
40
+
41
+ def jalali_to_gregorian(j: jdatetime.date) -> date:
42
+ """تبدیل تاریخ شمسی به میلادی."""
43
+ return j.togregorian()
44
+
45
+
46
+ def format_jalali(j: jdatetime.date) -> str:
47
+ """قالب‌بندی تاریخ شمسی به صورت YYYY/MM/DD."""
48
+ return f"{j.year:04d}/{j.month:02d}/{j.day:02d}"
49
+
50
+
51
+ def format_gregorian(g: date) -> str:
52
+ """قالب‌بندی تاریخ میلادی به صورت YYYY-MM-DD."""
53
+ return g.strftime("%Y-%m-%d")
54
+
55
+
56
+ def diff_days(a: date, b: date) -> int:
57
+ """اختلاف روز بین دو تاریخ (b - a)."""
58
+ return (b - a).days
@@ -0,0 +1,144 @@
1
+ """تولید داده‌های نمونهٔ فارسی (Lorem، نام، شهر)."""
2
+ from __future__ import annotations
3
+
4
+ import secrets
5
+
6
+ # متن لورم فارسی — جملات معنادار برای پر کردن قالب‌ها
7
+ _LOREM_SENTENCES = [
8
+ "در این روزگار، فناوری به سرعت در حال تغییر است.",
9
+ "برنامه‌نویسی هنری است که نیازمند صبر و دقت است.",
10
+ "هر مسئله‌ای راه‌حلی دارد، فقط باید آن را پیدا کرد.",
11
+ "کتاب‌ها بهترین دوستان انسان در تنهایی هستند.",
12
+ "زبان فارسی گنجینه‌ای از ادبیات و فرهنگ است.",
13
+ "کار تیمی باعث می‌شود کارهای بزرگ به ثمر بنشینند.",
14
+ "یادگیری مداوم کلید موفقیت در هر حرفه‌ای است.",
15
+ "طراحی خوب نامرئی است؛ کاربر فقط راحتی را حس می‌کند.",
16
+ "داده‌ها اگر درست تحلیل شوند، به دانش تبدیل می‌شوند.",
17
+ "خلاقیت وقتی شکوفا می‌شود که ذهن آزاد باشد.",
18
+ "کیفیت مهم‌تر از کمیت است در هر کاری.",
19
+ "احترام به دیگران، اولین قدم در ساختن جامعه‌ای بهتر است.",
20
+ "سادگی بالاترین درجهٔ پیچیدگی است.",
21
+ "آینده از آن کسانی است که امروز یاد می‌گیرند.",
22
+ "همکاری بین‌المللی می‌تواند بسیاری از مشکلات را حل کند.",
23
+ ]
24
+
25
+ _FIRST_NAMES_MALE = [
26
+ "علی", "محمد", "حسن", "حسین", "رضا", "امیر", "مهدی", "سعید", "احمد", "کاوه",
27
+ "بهرام", "داریوش", "کوروش", "سیاوش", "آرش", "فریدون", "نوید", "پیمان", "بابک",
28
+ "شاهین", "کیوان", "اردشیر", "پیروز", "فرهاد", "ساسان", "یاسر", "مانی",
29
+ ]
30
+
31
+ _FIRST_NAMES_FEMALE = [
32
+ "فاطمه", "زهرا", "مریم", "سارا", "نرگس", "الهام", "نازنین", "شیما", "پریسا", "نازلی",
33
+ "نیلوفر", "شیرین", "لیلا", "ملیکا", "آیدا", "بهاره", "گلنار", "رعنا", "کتایون",
34
+ "رودابه", "تهمینه", "فرنگیس", "یاسمن", "آزاده", "سیمین", "پروین", "مینا", "هستی",
35
+ ]
36
+
37
+ _LAST_NAMES = [
38
+ "احمدی", "محمدی", "رضایی", "حسینی", "موسوی", "کریمی", "صادقی", "جعفری", "قاسمی",
39
+ "حیدری", "نجفی", "محمودی", "اکبری", "قربانی", "رحیمی", "شریفی", "نوری", "کاظمی",
40
+ "زمانی", "مهدوی", "مرادی", "پورمحمدی", "سلطانی", "فرهادی", "امینی", "رستمی",
41
+ "بهرامی", "صالحی", "طاهری", "یزدانی", "جهانگیری", "خسروی", "زندی", "سپهری",
42
+ ]
43
+
44
+ _CITIES = [
45
+ "تهران", "مشهد", "اصفهان", "کرج", "شیراز", "تبریز", "قم", "اهواز", "کرمانشاه",
46
+ "ارومیه", "رشت", "زاهدان", "همدان", "کرمان", "یزد", "اردبیل", "بندرعباس",
47
+ "اراک", "قزوین", "زنجان", "سنندج", "گرگان", "ساری", "بوشهر", "بیرجند",
48
+ "خرم‌آباد", "ایلام", "سمنان", "یاسوج", "شهرکرد",
49
+ ]
50
+
51
+ _DOMAINS = ["example.com", "test.ir", "demo.ir", "sample.org", "fake.net"]
52
+
53
+
54
+ #lorem
55
+
56
+
57
+ def random_sentence() -> str:
58
+ """یک جملهٔ تصادفی فارسی."""
59
+ return secrets.choice(_LOREM_SENTENCES)
60
+
61
+
62
+ def random_paragraph(sentences: int = 4) -> str:
63
+ """پاراگراف با تعداد جملهٔ مشخص."""
64
+ if sentences < 1:
65
+ sentences = 1
66
+ chosen = [secrets.choice(_LOREM_SENTENCES) for _ in range(sentences)]
67
+ return " ".join(chosen)
68
+
69
+
70
+ def lorem_ipsum(paragraphs: int = 1, sentences_per_paragraph: int = 4) -> str:
71
+ """متن لورم فارسی با تعداد پاراگراف مشخص."""
72
+ if paragraphs < 1:
73
+ paragraphs = 1
74
+ return "\n\n".join(
75
+ random_paragraph(sentences_per_paragraph) for _ in range(paragraphs)
76
+ )
77
+
78
+
79
+ def random_words(count: int = 5) -> str:
80
+ """چند کلمهٔ تصادفی از متن لورم."""
81
+ all_words: list[str] = []
82
+ for sent in _LOREM_SENTENCES:
83
+ all_words.extend(sent.split())
84
+ if count < 1:
85
+ count = 1
86
+ return " ".join(secrets.choice(all_words) for _ in range(count))
87
+
88
+
89
+ #name
90
+
91
+
92
+ def random_first_name(gender: str = "any") -> str:
93
+ """نام کوچک تصادفی فارسی. gender: any|male|female"""
94
+ if gender == "male":
95
+ return secrets.choice(_FIRST_NAMES_MALE)
96
+ if gender == "female":
97
+ return secrets.choice(_FIRST_NAMES_FEMALE)
98
+ return secrets.choice(_FIRST_NAMES_MALE + _FIRST_NAMES_FEMALE)
99
+
100
+
101
+ def random_last_name() -> str:
102
+ """نام خانوادگی تصادفی فارسی."""
103
+ return secrets.choice(_LAST_NAMES)
104
+
105
+
106
+ def random_full_name(gender: str = "any") -> str:
107
+ """نام و نام خانوادگی فارسی."""
108
+ return f"{random_first_name(gender)} {random_last_name()}"
109
+
110
+
111
+ def random_city() -> str:
112
+ """نام شهر ایرانی."""
113
+ return secrets.choice(_CITIES)
114
+
115
+
116
+ def random_email(first: str | None = None, last: str | None = None) -> str:
117
+ """ایمیل تصادفی (بر پایهٔ نام)."""
118
+ if first is None:
119
+ first = random_first_name()
120
+ if last is None:
121
+ last = random_last_name()
122
+
123
+ # تبدیل به ASCII برای سازگاری با ایمیل
124
+ first_ascii = _transliterate(first)
125
+ last_ascii = _transliterate(last)
126
+ num = secrets.randbelow(1000)
127
+ domain = secrets.choice(_DOMAINS)
128
+ return f"{first_ascii}.{last_ascii}{num}@{domain}".lower()
129
+
130
+
131
+ _TRANSLIT_MAP = {
132
+ "ع": "a", "ا": "a", "ب": "b", "پ": "p", "ت": "t", "ث": "s", "ج": "j",
133
+ "چ": "ch", "ح": "h", "خ": "kh", "د": "d", "ذ": "z", "ر": "r", "ز": "z",
134
+ "ژ": "zh", "س": "s", "ش": "sh", "ص": "s", "ض": "z", "ط": "t", "ظ": "z",
135
+ "غ": "gh", "ف": "f", "ق": "gh", "ک": "k", "ك": "k", "گ": "g", "ل": "l",
136
+ "م": "m", "ن": "n", "و": "v", "ه": "h", "ی": "y", "ي": "y", "ء": "",
137
+ "آ": "a", "أ": "a", "إ": "e", "ؤ": "o", "ئ": "y", "\u200c": "",
138
+ " ": "",
139
+ }
140
+
141
+
142
+ def _transliterate(text: str) -> str:
143
+ """تبدیل سادهٔ حروف فارسی به لاتین (برای ایمیل و slug)."""
144
+ return "".join(_TRANSLIT_MAP.get(c, c) for c in text)