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.
- persian_devkit/__init__.py +4 -0
- persian_devkit/__main__.py +5 -0
- persian_devkit/cli.py +55 -0
- persian_devkit/commands/__init__.py +16 -0
- persian_devkit/commands/base64_cmd.py +51 -0
- persian_devkit/commands/color_cmd.py +143 -0
- persian_devkit/commands/csv_cmd.py +121 -0
- persian_devkit/commands/date_cmd.py +98 -0
- persian_devkit/commands/env_cmd.py +100 -0
- persian_devkit/commands/gitignore_cmd.py +68 -0
- persian_devkit/commands/hash_cmd.py +71 -0
- persian_devkit/commands/image_cmd.py +88 -0
- persian_devkit/commands/jq_cmd.py +92 -0
- persian_devkit/commands/json_cmd.py +135 -0
- persian_devkit/commands/license_cmd.py +67 -0
- persian_devkit/commands/lorem_cmd.py +53 -0
- persian_devkit/commands/name_cmd.py +98 -0
- persian_devkit/commands/number_cmd.py +65 -0
- persian_devkit/commands/password_cmd.py +55 -0
- persian_devkit/commands/qrcode_cmd.py +46 -0
- persian_devkit/commands/random_cmd.py +110 -0
- persian_devkit/commands/scaffold_cmd.py +155 -0
- persian_devkit/commands/template_cmd.py +87 -0
- persian_devkit/commands/text_cmd.py +157 -0
- persian_devkit/commands/time_cmd.py +96 -0
- persian_devkit/commands/toml_cmd.py +85 -0
- persian_devkit/commands/url_cmd.py +102 -0
- persian_devkit/commands/uuid_cmd.py +45 -0
- persian_devkit/commands/yaml_cmd.py +94 -0
- persian_devkit/main.py +63 -0
- persian_devkit/py.typed +0 -0
- persian_devkit/utils/__init__.py +1 -0
- persian_devkit/utils/color_utils.py +112 -0
- persian_devkit/utils/crypto_utils.py +157 -0
- persian_devkit/utils/data_utils.py +146 -0
- persian_devkit/utils/date_utils.py +58 -0
- persian_devkit/utils/fake_utils.py +144 -0
- persian_devkit/utils/gitignore_data.py +211 -0
- persian_devkit/utils/image_utils.py +37 -0
- persian_devkit/utils/jsonpath_utils.py +94 -0
- persian_devkit/utils/license_data.py +190 -0
- persian_devkit/utils/number_utils.py +78 -0
- persian_devkit/utils/qrcode_utils.py +84 -0
- persian_devkit/utils/text_utils.py +166 -0
- persian_devkit/utils/time_utils.py +81 -0
- persian_devkit/utils/url_utils.py +54 -0
- persian_devkit-1.0.0.dist-info/METADATA +702 -0
- persian_devkit-1.0.0.dist-info/RECORD +51 -0
- persian_devkit-1.0.0.dist-info/WHEEL +4 -0
- persian_devkit-1.0.0.dist-info/entry_points.txt +2 -0
- persian_devkit-1.0.0.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""توابع کمکی پاکسازی و پردازش متن فارسی."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
# ی و ک عربی به فارسی
|
|
7
|
+
_ARABIC_TO_PERSIAN = str.maketrans(
|
|
8
|
+
{
|
|
9
|
+
"\u064a": "\u06cc", # ي → ی
|
|
10
|
+
"\u0643": "\u06a9", # ك → ک
|
|
11
|
+
"\u0649": "\u06cc", # ى → ی
|
|
12
|
+
"\u0629": "\u0647", # ة → ه
|
|
13
|
+
"\u06c0": "\u0647", # ۀ → ه
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_DIACRITICS_RE = re.compile(r"[\u064b-\u065f\u0670]")
|
|
18
|
+
_MULTI_SPACE_RE = re.compile(r"[ \t]+")
|
|
19
|
+
_MULTI_NEWLINE_RE = re.compile(r"\n{3,}")
|
|
20
|
+
_SENTENCE_END_RE = re.compile(r"[.!?؟]+")
|
|
21
|
+
|
|
22
|
+
_ZWNJ = "\u200c"
|
|
23
|
+
_PREFIXES = ("نمی", "می") # ترتیب مهم است (بلندتر اول)
|
|
24
|
+
_SUFFIXES = ("هایی", "های", "ترین", "ها", "تر")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fix_halfspace(text: str) -> str:
|
|
28
|
+
"""اصلاح نیمفاصله در متن فارسی (میروم، کتابها، بهترترین)."""
|
|
29
|
+
result = text
|
|
30
|
+
# پیشوندها: می روم → میروم
|
|
31
|
+
for prefix in _PREFIXES:
|
|
32
|
+
result = re.sub(rf"(?<!\S){re.escape(prefix)}\s+", rf"{prefix}{_ZWNJ}", result)
|
|
33
|
+
# پسوندها: کتاب ها → کتابها
|
|
34
|
+
suffix_pattern = "|".join(re.escape(s) for s in _SUFFIXES)
|
|
35
|
+
result = re.sub(rf"\s+({suffix_pattern})(?!\S)", rf"{_ZWNJ}\1", result)
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalize(text: str) -> str:
|
|
40
|
+
"""یکسانسازی حروف عربی به فارسی، حذف اعراب و فاصلههای اضافی."""
|
|
41
|
+
result = text.translate(_ARABIC_TO_PERSIAN)
|
|
42
|
+
result = _DIACRITICS_RE.sub("", result)
|
|
43
|
+
result = _MULTI_SPACE_RE.sub(" ", result)
|
|
44
|
+
result = _MULTI_NEWLINE_RE.sub("\n\n", result)
|
|
45
|
+
return result.strip()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def reverse_text(text: str) -> str:
|
|
49
|
+
"""معکوسسازی کاراکترهای متن."""
|
|
50
|
+
return text[::-1]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def text_stats(text: str) -> dict[str, int]:
|
|
54
|
+
"""محاسبهٔ آمار متن: کاراکتر، کلمه، خط، جمله."""
|
|
55
|
+
lines = text.splitlines() or [""]
|
|
56
|
+
words = text.split()
|
|
57
|
+
chars = len(text)
|
|
58
|
+
chars_no_space = sum(1 for c in text if not c.isspace())
|
|
59
|
+
sentences = len(_SENTENCE_END_RE.findall(text))
|
|
60
|
+
if sentences == 0 and text.strip():
|
|
61
|
+
sentences = 1
|
|
62
|
+
return {
|
|
63
|
+
"chars": chars,
|
|
64
|
+
"chars_no_space": chars_no_space,
|
|
65
|
+
"words": len(words),
|
|
66
|
+
"lines": len(lines),
|
|
67
|
+
"sentences": sentences,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#slug & case
|
|
71
|
+
|
|
72
|
+
_SLUG_KEEP = re.compile(r"[^\w\s\-]", re.UNICODE)
|
|
73
|
+
_SLUG_SPACES = re.compile(r"[\s\-_]+")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def to_slug(text: str, separator: str = "-", max_length: int = 0) -> str:
|
|
77
|
+
"""تبدیل متن (فارسی یا انگلیسی) به slug قابل استفاده در URL.
|
|
78
|
+
|
|
79
|
+
نیمفاصله، اعراب و کاراکترهای غیرمجاز حذف یا جایگزین میشوند.
|
|
80
|
+
"""
|
|
81
|
+
# اول نرمالسازی ی/ک عربی
|
|
82
|
+
result = text.translate(_ARABIC_TO_PERSIAN)
|
|
83
|
+
# حذف اعراب
|
|
84
|
+
result = _DIACRITICS_RE.sub("", result)
|
|
85
|
+
# نیمفاصله به فاصله (چون بعداً با separator یکی میشود)
|
|
86
|
+
result = result.replace(_ZWNJ, " ")
|
|
87
|
+
# حذف کاراکترهای غیرمجاز (نگهداشتن حروف، اعداد، خط تیره، آندرلاین)
|
|
88
|
+
result = _SLUG_KEEP.sub("", result)
|
|
89
|
+
# فاصلهها و جداکنندههای تکراری
|
|
90
|
+
result = _SLUG_SPACES.sub(separator, result).strip(separator)
|
|
91
|
+
result = result.lower()
|
|
92
|
+
if max_length > 0 and len(result) > max_length:
|
|
93
|
+
result = result[:max_length].rstrip(separator)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
_CASE_MODES = ("upper", "lower", "title", "capitalize", "swap")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def change_case(text: str, mode: str) -> str:
|
|
101
|
+
"""تغییر حالت حروف متن.
|
|
102
|
+
|
|
103
|
+
حالتهای پشتیبانیشده: upper, lower, title, capitalize, swap
|
|
104
|
+
"""
|
|
105
|
+
if mode == "upper":
|
|
106
|
+
return text.upper()
|
|
107
|
+
if mode == "lower":
|
|
108
|
+
return text.lower()
|
|
109
|
+
if mode == "title":
|
|
110
|
+
return text.title()
|
|
111
|
+
if mode == "capitalize":
|
|
112
|
+
return text.capitalize()
|
|
113
|
+
if mode == "swap":
|
|
114
|
+
return text.swapcase()
|
|
115
|
+
raise ValueError(f"حالت ناشناخته: {mode}. مجاز: {', '.join(_CASE_MODES)}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
#diff
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def diff_lines(a: str, b: str) -> list[tuple[str, str]]:
|
|
122
|
+
"""مقایسهٔ خطی دو متن.
|
|
123
|
+
|
|
124
|
+
خروجی: لیستی از (علامت, خط) که علامت یکی از ' ', '-', '+' است.
|
|
125
|
+
"""
|
|
126
|
+
import difflib
|
|
127
|
+
|
|
128
|
+
a_lines = a.splitlines(keepends=False)
|
|
129
|
+
b_lines = b.splitlines(keepends=False)
|
|
130
|
+
result: list[tuple[str, str]] = []
|
|
131
|
+
for line in difflib.unified_diff(a_lines, b_lines, lineterm="", n=0):
|
|
132
|
+
if line.startswith("---") or line.startswith("+++"):
|
|
133
|
+
continue
|
|
134
|
+
if line.startswith("@@"):
|
|
135
|
+
result.append((" ", line))
|
|
136
|
+
elif line.startswith("-"):
|
|
137
|
+
result.append(("-", line[1:]))
|
|
138
|
+
elif line.startswith("+"):
|
|
139
|
+
result.append(("+", line[1:]))
|
|
140
|
+
else:
|
|
141
|
+
result.append((" ", line[1:] if line.startswith(" ") else line))
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
#regex
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def regex_test(pattern: str, text: str) -> dict:
|
|
149
|
+
"""اجرای regex روی متن و برگرداندن نتایج.
|
|
150
|
+
|
|
151
|
+
خروجی: دیکشنری شامل match, groups و تعداد.
|
|
152
|
+
"""
|
|
153
|
+
import re as _re
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
compiled = _re.compile(pattern)
|
|
157
|
+
except _re.error as exc:
|
|
158
|
+
raise ValueError(f"الگوی regex نامعتبر: {exc}") from exc
|
|
159
|
+
|
|
160
|
+
matches = compiled.findall(text)
|
|
161
|
+
return {
|
|
162
|
+
"pattern": pattern,
|
|
163
|
+
"count": len(matches),
|
|
164
|
+
"matches": [str(m) for m in matches],
|
|
165
|
+
"is_match": bool(compiled.search(text)),
|
|
166
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""توابع کمکی کار با زمان و timestamp."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
_DURATION_RE = re.compile(
|
|
8
|
+
r"(?P<value>\d+)\s*(?P<unit>s|m|h|d|w|ثانیه|دقیقه|ساعت|روز|هفته)",
|
|
9
|
+
re.IGNORECASE,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
_UNIT_SECONDS = {
|
|
13
|
+
"s": 1,
|
|
14
|
+
"m": 60,
|
|
15
|
+
"h": 3600,
|
|
16
|
+
"d": 86400,
|
|
17
|
+
"w": 604800,
|
|
18
|
+
"ثانیه": 1,
|
|
19
|
+
"دقیقه": 60,
|
|
20
|
+
"ساعت": 3600,
|
|
21
|
+
"روز": 86400,
|
|
22
|
+
"هفته": 604800,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def now_unix() -> int:
|
|
27
|
+
"""timestamp فعلی به ثانیه."""
|
|
28
|
+
return int(datetime.now(tz=timezone.utc).timestamp())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_unix(dt: datetime) -> int:
|
|
32
|
+
"""تبدیل datetime به timestamp (اگر timezone نداشته باشد، UTC فرض میشود)."""
|
|
33
|
+
if dt.tzinfo is None:
|
|
34
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
35
|
+
return int(dt.timestamp())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def from_unix(ts: int) -> datetime:
|
|
39
|
+
"""تبدیل timestamp به datetime (UTC)."""
|
|
40
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse_duration(text: str) -> int:
|
|
44
|
+
"""تبدیل رشتهٔ مدت زمان (مثل '2h30m' یا '2 ساعت و 30 دقیقه') به ثانیه."""
|
|
45
|
+
if not text or not text.strip():
|
|
46
|
+
raise ValueError("رشتهٔ مدت زمان خالی است.")
|
|
47
|
+
|
|
48
|
+
total = 0
|
|
49
|
+
found = False
|
|
50
|
+
for match in _DURATION_RE.finditer(text):
|
|
51
|
+
value = int(match.group("value"))
|
|
52
|
+
unit = match.group("unit").lower()
|
|
53
|
+
if unit not in _UNIT_SECONDS:
|
|
54
|
+
raise ValueError(f"واحد ناشناخته: {unit}")
|
|
55
|
+
total += value * _UNIT_SECONDS[unit]
|
|
56
|
+
found = True
|
|
57
|
+
|
|
58
|
+
if not found:
|
|
59
|
+
raise ValueError(f"فرمت مدت زمان نامعتبر: {text}")
|
|
60
|
+
return total
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def humanize_duration(seconds: int) -> str:
|
|
64
|
+
"""تبدیل ثانیه به رشتهٔ خوانا (فارسی)."""
|
|
65
|
+
if seconds < 0:
|
|
66
|
+
return "منفی " + humanize_duration(-seconds)
|
|
67
|
+
if seconds == 0:
|
|
68
|
+
return "۰ ثانیه"
|
|
69
|
+
|
|
70
|
+
parts: list[str] = []
|
|
71
|
+
for label, unit in (
|
|
72
|
+
("هفته", 604800),
|
|
73
|
+
("روز", 86400),
|
|
74
|
+
("ساعت", 3600),
|
|
75
|
+
("دقیقه", 60),
|
|
76
|
+
("ثانیه", 1),
|
|
77
|
+
):
|
|
78
|
+
if seconds >= unit:
|
|
79
|
+
value, seconds = divmod(seconds, unit)
|
|
80
|
+
parts.append(f"{value} {label}")
|
|
81
|
+
return " و ".join(parts)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""توابع کمکی کار با URL."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from urllib.parse import (
|
|
5
|
+
parse_qs,
|
|
6
|
+
parse_qsl,
|
|
7
|
+
quote,
|
|
8
|
+
unquote,
|
|
9
|
+
urlencode,
|
|
10
|
+
urlparse,
|
|
11
|
+
urlunparse,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def url_encode(text: str, safe: str = "") -> str:
|
|
16
|
+
"""درصد-رمزگذاری متن برای استفاده در URL."""
|
|
17
|
+
return quote(text, safe=safe)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def url_decode(text: str) -> str:
|
|
21
|
+
"""رمزگشایی متن درصد-رمزگذاریشده."""
|
|
22
|
+
return unquote(text)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_url(url: str) -> dict:
|
|
26
|
+
"""تجزیهٔ URL به اجزای سازنده."""
|
|
27
|
+
parsed = urlparse(url)
|
|
28
|
+
query = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
|
29
|
+
return {
|
|
30
|
+
"scheme": parsed.scheme,
|
|
31
|
+
"netloc": parsed.netloc,
|
|
32
|
+
"host": parsed.hostname or "",
|
|
33
|
+
"port": parsed.port,
|
|
34
|
+
"path": parsed.path,
|
|
35
|
+
"query": query,
|
|
36
|
+
"fragment": parsed.fragment,
|
|
37
|
+
"username": parsed.username,
|
|
38
|
+
"password": parsed.password,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_query(params: dict[str, str]) -> str:
|
|
43
|
+
"""ساخت query string از دیکشنری."""
|
|
44
|
+
return urlencode(params)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def encode_query(params: dict[str, str]) -> str:
|
|
48
|
+
"""مترادف build_query برای خوانایی."""
|
|
49
|
+
return build_query(params)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def decode_query(text: str) -> dict[str, list[str]]:
|
|
53
|
+
"""تجزیهٔ query string به دیکشنری (مقادیر چندگانه → لیست)."""
|
|
54
|
+
return parse_qs(text, keep_blank_values=True)
|