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,55 @@
|
|
|
1
|
+
"""دستور pdev password — تولید و سنجش رمز عبور."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from persian_devkit.utils.crypto_utils import (
|
|
9
|
+
generate_password,
|
|
10
|
+
password_strength,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="تولید و سنجش رمز عبور.", no_args_is_help=True)
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("new")
|
|
18
|
+
def new_cmd(
|
|
19
|
+
length: int = typer.Option(16, "--length", "-l", min=4, max=256, help="طول رمز."),
|
|
20
|
+
no_symbols: bool = typer.Option(False, "--no-symbols", help="بدون نمادها."),
|
|
21
|
+
no_upper: bool = typer.Option(False, "--no-upper", help="بدون حروف بزرگ."),
|
|
22
|
+
no_digits: bool = typer.Option(False, "--no-digits", help="بدون ارقام."),
|
|
23
|
+
count: int = typer.Option(1, "--count", "-c", min=1, max=100, help="تعداد."),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""تولید رمز عبور امن."""
|
|
26
|
+
try:
|
|
27
|
+
for _ in range(count):
|
|
28
|
+
console.print(
|
|
29
|
+
generate_password(
|
|
30
|
+
length=length,
|
|
31
|
+
use_upper=not no_upper,
|
|
32
|
+
use_digits=not no_digits,
|
|
33
|
+
use_symbols=not no_symbols,
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
except ValueError as e:
|
|
37
|
+
console.print(f"[red]✗ خطا:[/red] {e}")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("check")
|
|
42
|
+
def check_cmd(
|
|
43
|
+
password: str = typer.Argument(..., help="رمز برای سنجش قدرت."),
|
|
44
|
+
) -> None:
|
|
45
|
+
"""سنجش قدرت رمز عبور."""
|
|
46
|
+
result = password_strength(password)
|
|
47
|
+
|
|
48
|
+
color = "red" if result["score"] < 40 else "yellow" if result["score"] < 70 else "green"
|
|
49
|
+
table = Table(title=" قدرت رمز", title_style="bold cyan")
|
|
50
|
+
table.add_column("شاخص", style="bold")
|
|
51
|
+
table.add_column("مقدار")
|
|
52
|
+
table.add_row("امتیاز", f"[{color}]{result['score']}/100[/{color}]")
|
|
53
|
+
table.add_row("برچسب", f"[{color}]{result['label']}[/{color}]")
|
|
54
|
+
table.add_row("آنتروپی", f"{result['entropy']} بیت")
|
|
55
|
+
console.print(table)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""دستور pdev qrcode — تولید QR Code از متن."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from persian_devkit.utils.qrcode_utils import qr_to_ascii, qr_to_image
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def qrcode_command(
|
|
16
|
+
text: str = typer.Argument(..., help="متن یا URL برای رمزگذاری."),
|
|
17
|
+
output: Optional[Path] = typer.Option(
|
|
18
|
+
None, "--output", "-o", help="ذخیره به فایل تصویری (پسوند از نام)."
|
|
19
|
+
),
|
|
20
|
+
error: str = typer.Option(
|
|
21
|
+
"M", "--error", "-e", help="سطح تصحیح خطا: L, M, Q, H."
|
|
22
|
+
),
|
|
23
|
+
box_size: int = typer.Option(
|
|
24
|
+
10, "--box-size", "-b", min=1, max=50, help="اندازهٔ هر بلوک (فقط برای فایل)."
|
|
25
|
+
),
|
|
26
|
+
border: int = typer.Option(
|
|
27
|
+
2, "--border", min=0, max=10, help="حاشیه (فقط برای فایل)."
|
|
28
|
+
),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""تولید QR Code از متن (فارسی یا انگلیسی)."""
|
|
31
|
+
if output is not None:
|
|
32
|
+
try:
|
|
33
|
+
qr_to_image(text, output, error=error, box_size=box_size, border=border)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
console.print(f"[red]✗ خطا:[/red] {e}")
|
|
36
|
+
raise typer.Exit(1)
|
|
37
|
+
console.print(f"[green]✓ ذخیره شد:[/green] {output}")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
art = qr_to_ascii(text, error=error, border=1)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
console.print(f"[red]✗ خطا:[/red] {e}")
|
|
44
|
+
raise typer.Exit(1)
|
|
45
|
+
|
|
46
|
+
console.print(art, highlight=False, markup=False, soft_wrap=True)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""دستور pdev random — تولید مقادیر تصادفی."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import secrets
|
|
5
|
+
import string
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from persian_devkit.utils.crypto_utils import nanoid
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="تولید مقادیر تصادفی.", no_args_is_help=True)
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("int")
|
|
17
|
+
def int_cmd(
|
|
18
|
+
low: int = typer.Argument(0, help="کران پایین (شامل)."),
|
|
19
|
+
high: int = typer.Argument(100, help="کران بالا (شامل)."),
|
|
20
|
+
count: int = typer.Option(1, "--count", "-c", min=1, help="تعداد."),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""تولید عدد صحیح تصادفی در بازه."""
|
|
23
|
+
if low > high:
|
|
24
|
+
console.print("[red]✗ خطا:[/red] کران پایین بزرگتر از بالا است.")
|
|
25
|
+
raise typer.Exit(1)
|
|
26
|
+
for _ in range(count):
|
|
27
|
+
console.print(secrets.randbelow(high - low + 1) + low)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command("str")
|
|
31
|
+
def str_cmd(
|
|
32
|
+
length: int = typer.Option(16, "--length", "-l", min=1, help="طول."),
|
|
33
|
+
count: int = typer.Option(1, "--count", "-c", min=1, help="تعداد."),
|
|
34
|
+
alphabet: str = typer.Option(
|
|
35
|
+
"full",
|
|
36
|
+
"--alphabet",
|
|
37
|
+
"-a",
|
|
38
|
+
help="مجموعهٔ کاراکتر: letters, digits, hex, full.",
|
|
39
|
+
),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""تولید رشتهٔ تصادفی."""
|
|
42
|
+
pools = {
|
|
43
|
+
"letters": string.ascii_letters,
|
|
44
|
+
"digits": string.digits,
|
|
45
|
+
"hex": "0123456789abcdef",
|
|
46
|
+
"full": string.ascii_letters + string.digits,
|
|
47
|
+
}
|
|
48
|
+
if alphabet not in pools:
|
|
49
|
+
console.print(f"[red]✗ خطا:[/red] مجموعهٔ ناشناخته: {alphabet}")
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
pool = pools[alphabet]
|
|
52
|
+
for _ in range(count):
|
|
53
|
+
console.print("".join(secrets.choice(pool) for _ in range(length)))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command("choice")
|
|
57
|
+
def choice_cmd(
|
|
58
|
+
items: list[str] = typer.Argument(..., help="گزینهها برای انتخاب."),
|
|
59
|
+
count: int = typer.Option(1, "--count", "-c", min=1, help="تعداد انتخاب."),
|
|
60
|
+
replace: bool = typer.Option(
|
|
61
|
+
False, "--replace", "-r", help="انتخاب با جایگذاری (تکرار مجاز)."
|
|
62
|
+
),
|
|
63
|
+
) -> None:
|
|
64
|
+
"""انتخاب تصادفی از میان گزینهها."""
|
|
65
|
+
if count > len(items) and not replace:
|
|
66
|
+
console.print(
|
|
67
|
+
"[red]✗ خطا:[/red] تعداد بیشتر از گزینههاست. از --replace استفاده کن."
|
|
68
|
+
)
|
|
69
|
+
raise typer.Exit(1)
|
|
70
|
+
if replace:
|
|
71
|
+
for _ in range(count):
|
|
72
|
+
console.print(secrets.choice(items))
|
|
73
|
+
else:
|
|
74
|
+
# Fisher-Yates با secrets
|
|
75
|
+
pool = list(items)
|
|
76
|
+
for i in range(count):
|
|
77
|
+
j = i + secrets.randbelow(len(pool) - i)
|
|
78
|
+
pool[i], pool[j] = pool[j], pool[i]
|
|
79
|
+
console.print(pool[i])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command("coin")
|
|
83
|
+
def coin_cmd(
|
|
84
|
+
count: int = typer.Option(1, "--count", "-c", min=1, help="تعداد پرتاب."),
|
|
85
|
+
heads_label: str = typer.Option("شیر", "--heads"),
|
|
86
|
+
tails_label: str = typer.Option("خط", "--tails"),
|
|
87
|
+
) -> None:
|
|
88
|
+
"""پرتاب سکه."""
|
|
89
|
+
for _ in range(count):
|
|
90
|
+
console.print(heads_label if secrets.randbelow(2) == 0 else tails_label)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command("dice")
|
|
94
|
+
def dice_cmd(
|
|
95
|
+
sides: int = typer.Option(6, "--sides", "-s", min=2, help="تعداد وجه."),
|
|
96
|
+
count: int = typer.Option(1, "--count", "-c", min=1, help="تعداد تاس."),
|
|
97
|
+
) -> None:
|
|
98
|
+
"""پرتاب تاس."""
|
|
99
|
+
for _ in range(count):
|
|
100
|
+
console.print(secrets.randbelow(sides) + 1)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command("nanoid")
|
|
104
|
+
def nanoid_cmd(
|
|
105
|
+
length: int = typer.Option(21, "--length", "-l", min=1, help="طول."),
|
|
106
|
+
count: int = typer.Option(1, "--count", "-c", min=1, help="تعداد."),
|
|
107
|
+
) -> None:
|
|
108
|
+
"""تولید Nano ID."""
|
|
109
|
+
for _ in range(count):
|
|
110
|
+
console.print(nanoid(length))
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""دستور pdev scaffold — ساخت ساختار پروژه از قالب."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# قالبهای پروژه: {filepath: content}
|
|
13
|
+
_SCAFFOLDS: dict[str, dict[str, str]] = {
|
|
14
|
+
"python": {
|
|
15
|
+
"README.md": "# {name}\n\nتوضیح پروژه...\n",
|
|
16
|
+
"pyproject.toml": """[build-system]
|
|
17
|
+
requires = ["hatchling"]
|
|
18
|
+
build-backend = "hatchling.build"
|
|
19
|
+
|
|
20
|
+
[project]
|
|
21
|
+
name = "{name}"
|
|
22
|
+
version = "0.1.0"
|
|
23
|
+
description = ""
|
|
24
|
+
readme = "README.md"
|
|
25
|
+
requires-python = ">=3.10"
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
{name} = "{name}.main:main"
|
|
30
|
+
""",
|
|
31
|
+
"requirements.txt": "",
|
|
32
|
+
"src/{name}/__init__.py": '"""پکیج {name}."""\n\n__version__ = "0.1.0"\n',
|
|
33
|
+
"src/{name}/main.py": '''"""نقطهٔ ورود {name}."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main() -> None:
|
|
37
|
+
"""تابع اصلی."""
|
|
38
|
+
print("سلام از {name}!")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|
|
43
|
+
''',
|
|
44
|
+
"tests/__init__.py": "",
|
|
45
|
+
"tests/test_main.py": '''"""تستهای {name}."""
|
|
46
|
+
from {name}.main import main
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_main_runs(capsys):
|
|
50
|
+
main()
|
|
51
|
+
captured = capsys.readouterr()
|
|
52
|
+
assert "{name}" in captured.out
|
|
53
|
+
''',
|
|
54
|
+
".gitignore": "__pycache__/\n*.pyc\n.venv/\nvenv/\ndist/\nbuild/\n*.egg-info/\n",
|
|
55
|
+
},
|
|
56
|
+
"node": {
|
|
57
|
+
"README.md": "# {name}\n\nتوضیح پروژه...\n",
|
|
58
|
+
"package.json": """{{
|
|
59
|
+
"name": "{name}",
|
|
60
|
+
"version": "0.1.0",
|
|
61
|
+
"description": "",
|
|
62
|
+
"main": "src/index.js",
|
|
63
|
+
"scripts": {{
|
|
64
|
+
"start": "node src/index.js",
|
|
65
|
+
"test": "echo \\"Error: no test specified\\" && exit 1"
|
|
66
|
+
}},
|
|
67
|
+
"keywords": [],
|
|
68
|
+
"author": "",
|
|
69
|
+
"license": "MIT"
|
|
70
|
+
}}
|
|
71
|
+
""",
|
|
72
|
+
"src/index.js": "console.log('سلام از {name}!');\n",
|
|
73
|
+
".gitignore": "node_modules/\ndist/\n.env\n.env.local\n",
|
|
74
|
+
},
|
|
75
|
+
"minimal": {
|
|
76
|
+
"README.md": "# {name}\n",
|
|
77
|
+
".gitignore": "__pycache__/\n.venv/\n.env\n",
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _create_files(root: Path, name: str, scaffold: dict[str, str]) -> list[Path]:
|
|
83
|
+
"""فایلهای قالب را میسازد و لیست مسیرها را برمیگرداند."""
|
|
84
|
+
created: list[Path] = []
|
|
85
|
+
for rel, content in scaffold.items():
|
|
86
|
+
path = root / rel.format(name=name)
|
|
87
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
rendered = content.format(name=name)
|
|
89
|
+
path.write_text(rendered, encoding="utf-8")
|
|
90
|
+
created.append(path)
|
|
91
|
+
return created
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def scaffold_command(
|
|
95
|
+
kind: Optional[str] = typer.Argument(
|
|
96
|
+
None, help="نوع قالب: python، node، minimal."
|
|
97
|
+
),
|
|
98
|
+
name: Optional[str] = typer.Argument(None, help="نام پروژه."),
|
|
99
|
+
target: Path = typer.Option(
|
|
100
|
+
Path("."), "--target", "-t", help="پوشهٔ مقصد (پیشفرض: جاری)."
|
|
101
|
+
),
|
|
102
|
+
force: bool = typer.Option(
|
|
103
|
+
False, "--force", "-f", help="اجرای اجباری روی پوشههای موجود."
|
|
104
|
+
),
|
|
105
|
+
list_templates: bool = typer.Option(
|
|
106
|
+
False, "--list", "-l", help="نمایش قالبهای موجود."
|
|
107
|
+
),
|
|
108
|
+
) -> None:
|
|
109
|
+
"""ساخت ساختار پروژه از قالب.
|
|
110
|
+
|
|
111
|
+
مثال:
|
|
112
|
+
pdev scaffold python myproject
|
|
113
|
+
pdev scaffold node myapp --target ./projects
|
|
114
|
+
pdev scaffold --list
|
|
115
|
+
"""
|
|
116
|
+
# حالت --list: نمایش قالبها و خروج
|
|
117
|
+
if list_templates:
|
|
118
|
+
console.print("قالبهای پروژه:")
|
|
119
|
+
for tname in _SCAFFOLDS:
|
|
120
|
+
console.print(f" • {tname}")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
if kind is None or name is None:
|
|
124
|
+
console.print("[red]✗ خطا:[/red] نوع قالب و نام پروژه الزامی است.")
|
|
125
|
+
console.print("برای دیدن قالبها: [bold]pdev scaffold --list[/bold]")
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
|
|
128
|
+
key = kind.lower()
|
|
129
|
+
if key not in _SCAFFOLDS:
|
|
130
|
+
console.print(
|
|
131
|
+
f"[red]✗ خطا:[/red] قالب ناشناخته: {kind}. "
|
|
132
|
+
f"موجود: {', '.join(_SCAFFOLDS.keys())}"
|
|
133
|
+
)
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
|
|
136
|
+
if not name or "/" in name or "\\" in name:
|
|
137
|
+
console.print(f"[red]✗ خطا:[/red] نام پروژه نامعتبر: {name}")
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
|
|
140
|
+
root = target.resolve() / name
|
|
141
|
+
if root.exists() and not force:
|
|
142
|
+
console.print(
|
|
143
|
+
f"[red]✗ خطا:[/red] پوشهٔ مقصد وجود دارد: {root} (از --force استفاده کن)"
|
|
144
|
+
)
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
created = _create_files(root, name, _SCAFFOLDS[key])
|
|
149
|
+
|
|
150
|
+
console.print(f"[green]✓ پروژه ساخته شد:[/green] {root}")
|
|
151
|
+
for p in created:
|
|
152
|
+
rel = p.relative_to(root)
|
|
153
|
+
console.print(f" • {rel}")
|
|
154
|
+
console.print(f"\n[cyan]قدم بعدی:[/cyan] cd {root}")
|
|
155
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""دستور pdev template — رندر قالب ساده با متغیر."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
# سینتکس: {{var}} یا {{ var }}
|
|
14
|
+
_VAR_RE = re.compile(r"\{\{\s*([A-Za-z_][\w\.\-]*)\s*\}\}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_template(text: str, variables: dict[str, str]) -> str:
|
|
18
|
+
"""جایگذاری متغیرها در قالب. متغیرهای تعریفنشده دستنخورده میمانند."""
|
|
19
|
+
def replacer(m: re.Match) -> str:
|
|
20
|
+
key = m.group(1)
|
|
21
|
+
return variables.get(key, m.group(0))
|
|
22
|
+
|
|
23
|
+
return _VAR_RE.sub(replacer, text)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def find_variables(text: str) -> list[str]:
|
|
27
|
+
"""لیست متغیرهای داخل قالب (بدون تکرار، به ترتیب ظهور)."""
|
|
28
|
+
seen: list[str] = []
|
|
29
|
+
for m in _VAR_RE.finditer(text):
|
|
30
|
+
key = m.group(1)
|
|
31
|
+
if key not in seen:
|
|
32
|
+
seen.append(key)
|
|
33
|
+
return seen
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def template_command(
|
|
37
|
+
file: Path = typer.Argument(..., help="مسیر فایل قالب."),
|
|
38
|
+
var: list[str] = typer.Option(
|
|
39
|
+
[], "--var", "-D", help="متغیر به شکل key=value (چند بار مجاز)."
|
|
40
|
+
),
|
|
41
|
+
output: Optional[Path] = typer.Option(
|
|
42
|
+
None, "--output", "-o", help="ذخیره در فایل خروجی."
|
|
43
|
+
),
|
|
44
|
+
show_vars: bool = typer.Option(
|
|
45
|
+
False, "--vars", "-V", help="فقط نمایش متغیرهای قالب (بدون رندر)."
|
|
46
|
+
),
|
|
47
|
+
) -> None:
|
|
48
|
+
"""رندر یک قالب متنی با متغیرها.
|
|
49
|
+
|
|
50
|
+
مثال:
|
|
51
|
+
pdev template greet.txt -D name=علی -D city=تهران
|
|
52
|
+
pdev template config.tpl -D port=8080 -o config.yaml
|
|
53
|
+
pdev template greet.txt --vars
|
|
54
|
+
"""
|
|
55
|
+
if not file.exists():
|
|
56
|
+
console.print(f"[red]✗ خطا:[/red] فایل یافت نشد: {file}")
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
|
|
59
|
+
text = file.read_text(encoding="utf-8")
|
|
60
|
+
|
|
61
|
+
# حالت --vars: فقط نمایش متغیرها
|
|
62
|
+
if show_vars:
|
|
63
|
+
found = find_variables(text)
|
|
64
|
+
if not found:
|
|
65
|
+
console.print("[yellow]⚠ هیچ متغیری یافت نشد.[/yellow]")
|
|
66
|
+
return
|
|
67
|
+
console.print(f"متغیرهای یافتشده ({len(found)}):")
|
|
68
|
+
for v in found:
|
|
69
|
+
console.print(f" • {v}")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# تجزیهٔ متغیرهای دادهشده
|
|
73
|
+
variables: dict[str, str] = {}
|
|
74
|
+
for item in var:
|
|
75
|
+
if "=" not in item:
|
|
76
|
+
console.print(f"[red]✗ خطا:[/red] فرمت نامعتبر: {item} (باید key=value)")
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
k, v = item.split("=", 1)
|
|
79
|
+
variables[k.strip()] = v
|
|
80
|
+
|
|
81
|
+
rendered = render_template(text, variables)
|
|
82
|
+
|
|
83
|
+
if output:
|
|
84
|
+
output.write_text(rendered, encoding="utf-8")
|
|
85
|
+
console.print(f"[green]✓ ذخیره شد:[/green] {output}")
|
|
86
|
+
else:
|
|
87
|
+
console.print(rendered, highlight=False, markup=False)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""دستور `pdev text` — پاکسازی و پردازش متن فارسی."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from persian_devkit.utils.text_utils import (
|
|
12
|
+
change_case,
|
|
13
|
+
diff_lines,
|
|
14
|
+
fix_halfspace,
|
|
15
|
+
normalize,
|
|
16
|
+
regex_test,
|
|
17
|
+
reverse_text,
|
|
18
|
+
text_stats,
|
|
19
|
+
to_slug,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(help="پاکسازی و پردازش متن فارسی.", no_args_is_help=True)
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _read_text(value: Optional[str]) -> str:
|
|
27
|
+
"""اگر مقدار داده نشد، از stdin میخواند."""
|
|
28
|
+
if value is not None and value != "":
|
|
29
|
+
return value
|
|
30
|
+
try:
|
|
31
|
+
if not sys.stdin.isatty():
|
|
32
|
+
data = sys.stdin.read()
|
|
33
|
+
if data.strip():
|
|
34
|
+
return data.rstrip("\n")
|
|
35
|
+
except (AttributeError, OSError):
|
|
36
|
+
pass
|
|
37
|
+
console.print(
|
|
38
|
+
"[red]✗ خطا:[/red] متنی وارد نشده. یک آرگومان بده یا از stdin استفاده کن."
|
|
39
|
+
)
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command("halfspace")
|
|
44
|
+
def halfspace_cmd(
|
|
45
|
+
text: Optional[str] = typer.Argument(
|
|
46
|
+
None, help="متن ورودی (اگر ندهی، از stdin خوانده میشود)."
|
|
47
|
+
),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""اصلاح نیمفاصله در متن فارسی."""
|
|
50
|
+
console.print(fix_halfspace(_read_text(text)))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("normalize")
|
|
54
|
+
def normalize_cmd(
|
|
55
|
+
text: Optional[str] = typer.Argument(
|
|
56
|
+
None, help="متن ورودی (اگر ندهی، از stdin خوانده میشود)."
|
|
57
|
+
),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""یکسانسازی حروف (ی/ک عربی) و حذف فاصلههای اضافه."""
|
|
60
|
+
console.print(normalize(_read_text(text)))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command("reverse")
|
|
64
|
+
def reverse_cmd(
|
|
65
|
+
text: Optional[str] = typer.Argument(
|
|
66
|
+
None, help="متن ورودی (اگر ندهی، از stdin خوانده میشود)."
|
|
67
|
+
),
|
|
68
|
+
) -> None:
|
|
69
|
+
"""معکوسسازی متن."""
|
|
70
|
+
console.print(reverse_text(_read_text(text)))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command("stats")
|
|
74
|
+
def stats_cmd(
|
|
75
|
+
text: Optional[str] = typer.Argument(
|
|
76
|
+
None, help="متن ورودی (اگر ندهی، از stdin خوانده میشود)."
|
|
77
|
+
),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""نمایش آمار متن (کاراکتر، کلمه، خط، جمله)."""
|
|
80
|
+
stats = text_stats(_read_text(text))
|
|
81
|
+
|
|
82
|
+
table = Table(title=" آمار متن", title_style="bold cyan")
|
|
83
|
+
table.add_column("شاخص", style="bold")
|
|
84
|
+
table.add_column("مقدار", justify="right", style="green")
|
|
85
|
+
table.add_row("کاراکترها", str(stats["chars"]))
|
|
86
|
+
table.add_row("کاراکترهای غیرفاصله", str(stats["chars_no_space"]))
|
|
87
|
+
table.add_row("کلمات", str(stats["words"]))
|
|
88
|
+
table.add_row("خطوط", str(stats["lines"]))
|
|
89
|
+
table.add_row("جملات", str(stats["sentences"]))
|
|
90
|
+
console.print(table)
|
|
91
|
+
|
|
92
|
+
@app.command("slug")
|
|
93
|
+
def slug_cmd(
|
|
94
|
+
text: Optional[str] = typer.Argument(None, help="متن ورودی (یا از stdin)."),
|
|
95
|
+
separator: str = typer.Option("-", "--sep", "-s", help="جداکننده."),
|
|
96
|
+
max_length: int = typer.Option(
|
|
97
|
+
0, "--max-length", "-m", min=0, help="حداکثر طول (۰ = بدون محدودیت)."
|
|
98
|
+
),
|
|
99
|
+
) -> None:
|
|
100
|
+
"""تبدیل متن به slug قابل استفاده در URL."""
|
|
101
|
+
console.print(to_slug(_read_text(text), separator=separator, max_length=max_length))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command("case")
|
|
105
|
+
def case_cmd(
|
|
106
|
+
mode: str = typer.Argument(
|
|
107
|
+
..., help="حالت: upper, lower, title, capitalize, swap."
|
|
108
|
+
),
|
|
109
|
+
text: Optional[str] = typer.Argument(None, help="متن ورودی (یا از stdin)."),
|
|
110
|
+
) -> None:
|
|
111
|
+
"""تغییر حالت حروف متن."""
|
|
112
|
+
try:
|
|
113
|
+
console.print(change_case(_read_text(text), mode))
|
|
114
|
+
except ValueError as e:
|
|
115
|
+
console.print(f"[red]✗ خطا:[/red] {e}")
|
|
116
|
+
raise typer.Exit(1)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command("diff")
|
|
120
|
+
def diff_cmd(
|
|
121
|
+
a: str = typer.Argument(..., help="متن اول."),
|
|
122
|
+
b: str = typer.Argument(..., help="متن دوم."),
|
|
123
|
+
) -> None:
|
|
124
|
+
"""مقایسهٔ خطی دو متن (مانند diff)."""
|
|
125
|
+
lines = diff_lines(a, b)
|
|
126
|
+
if not lines:
|
|
127
|
+
console.print("[green]✓ دو متن یکسان هستند.[/green]")
|
|
128
|
+
return
|
|
129
|
+
for sign, line in lines:
|
|
130
|
+
if sign == "-":
|
|
131
|
+
console.print(f"[red]- {line}[/red]")
|
|
132
|
+
elif sign == "+":
|
|
133
|
+
console.print(f"[green]+ {line}[/green]")
|
|
134
|
+
else:
|
|
135
|
+
console.print(f"[dim] {line}[/dim]")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command("regex")
|
|
139
|
+
def regex_cmd(
|
|
140
|
+
pattern: str = typer.Argument(..., help="الگوی regex."),
|
|
141
|
+
text: Optional[str] = typer.Argument(None, help="متن ورودی (یا از stdin)."),
|
|
142
|
+
) -> None:
|
|
143
|
+
"""اجرای الگوی regex روی متن و نمایش نتایج."""
|
|
144
|
+
try:
|
|
145
|
+
result = regex_test(pattern, _read_text(text))
|
|
146
|
+
except ValueError as e:
|
|
147
|
+
console.print(f"[red]✗ خطا:[/red] {e}")
|
|
148
|
+
raise typer.Exit(1)
|
|
149
|
+
|
|
150
|
+
if result["is_match"]:
|
|
151
|
+
console.print("[green]✓ مطابقت یافت شد.[/green]")
|
|
152
|
+
else:
|
|
153
|
+
console.print("[yellow]⚠ مطابقتی یافت نشد.[/yellow]")
|
|
154
|
+
|
|
155
|
+
console.print(f"تعداد: [bold]{result['count']}[/bold]")
|
|
156
|
+
for m in result["matches"][:20]:
|
|
157
|
+
console.print(f" • {m}")
|