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,96 @@
1
+ """دستور pdev time — کار با timestamp و مدت زمان."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timezone
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from persian_devkit.utils.time_utils import (
11
+ from_unix,
12
+ humanize_duration,
13
+ now_unix,
14
+ parse_duration,
15
+ to_unix,
16
+ )
17
+
18
+ app = typer.Typer(help="کار با timestamp و مدت زمان.", no_args_is_help=True)
19
+ console = Console()
20
+
21
+
22
+ @app.command("now")
23
+ def now_cmd(
24
+ ms: bool = typer.Option(False, "--ms", help="خروجی به میلی‌ثانیه."),
25
+ ) -> None:
26
+ """نمایش timestamp فعلی."""
27
+ ts = now_unix()
28
+ console.print(ts * 1000 if ms else ts)
29
+
30
+
31
+ @app.command("to-unix")
32
+ def to_unix_cmd(
33
+ value: str = typer.Argument(
34
+ ..., help="تاریخ به شکل YYYY-MM-DD یا YYYY-MM-DD HH:MM:SS."
35
+ ),
36
+ ) -> None:
37
+ """تبدیل تاریخ میلادی به timestamp."""
38
+ for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
39
+ try:
40
+ dt = datetime.strptime(value.strip(), fmt)
41
+ console.print(to_unix(dt))
42
+ return
43
+ except ValueError:
44
+ continue
45
+ console.print(f"[red]✗ خطا:[/red] فرمت تاریخ نامعتبر: {value}")
46
+ raise typer.Exit(1)
47
+
48
+
49
+ @app.command("from-unix")
50
+ def from_unix_cmd(
51
+ ts: int = typer.Argument(..., help="timestamp (ثانیه)."),
52
+ local: bool = typer.Option(False, "--local", help="نمایش در زمان محلی."),
53
+ ) -> None:
54
+ """تبدیل timestamp به تاریخ میلادی."""
55
+ dt = from_unix(ts)
56
+ if local:
57
+ dt = dt.astimezone()
58
+ console.print(dt.strftime("%Y-%m-%d %H:%M:%S %Z"))
59
+ else:
60
+ console.print(dt.strftime("%Y-%m-%d %H:%M:%S UTC"))
61
+
62
+
63
+ @app.command("duration")
64
+ def duration_cmd(
65
+ value: str = typer.Argument(
66
+ ..., help="مدت زمان (مثل '2h30m' یا '2 ساعت و 30 دقیقه')."
67
+ ),
68
+ ) -> None:
69
+ """تبدیل رشتهٔ مدت زمان به ثانیه و معادل خوانا."""
70
+ try:
71
+ seconds = parse_duration(value)
72
+ except ValueError as e:
73
+ console.print(f"[red]✗ خطا:[/red] {e}")
74
+ raise typer.Exit(1)
75
+
76
+ table = Table(title="⏱ مدت زمان", title_style="bold cyan", show_header=False)
77
+ table.add_column("شاخص", style="bold")
78
+ table.add_column("مقدار", style="green")
79
+ table.add_row("ثانیه", f"{seconds:,}")
80
+ table.add_row("خوانا", humanize_duration(seconds))
81
+ console.print(table)
82
+
83
+
84
+ @app.command("ago")
85
+ def ago_cmd(
86
+ ts: int = typer.Argument(..., help="timestamp در گذشته."),
87
+ ) -> None:
88
+ """نمایش فاصلهٔ زمانی از یک timestamp تا الان."""
89
+ now = now_unix()
90
+ delta = now - ts
91
+ if delta < 0:
92
+ console.print(
93
+ f"[yellow]⚠ این زمان در آینده است:[/yellow] {humanize_duration(-delta)} بعد"
94
+ )
95
+ else:
96
+ console.print(f"{humanize_duration(delta)} پیش")
@@ -0,0 +1,85 @@
1
+ """دستور pdev toml — کار با فایل‌های TOML."""
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.data_utils import (
11
+ dump_json,
12
+ dump_toml,
13
+ parse_json,
14
+ parse_toml,
15
+ read_text,
16
+ write_text,
17
+ )
18
+
19
+ app = typer.Typer(help="کار با فایل‌های TOML.", no_args_is_help=True)
20
+ console = Console()
21
+
22
+
23
+ def _fail(msg: str) -> None:
24
+ console.print(f"[red]✗ خطا:[/red] {msg}")
25
+ raise typer.Exit(1)
26
+
27
+
28
+ @app.command("validate")
29
+ def validate_cmd(
30
+ path: Path = typer.Argument(..., help="مسیر فایل TOML."),
31
+ ) -> None:
32
+ """بررسی صحت TOML."""
33
+ if not path.exists():
34
+ _fail(f"فایل یافت نشد: {path}")
35
+ try:
36
+ parse_toml(read_text(path))
37
+ except ValueError as e:
38
+ _fail(str(e))
39
+ console.print("[green]✓ TOML معتبر است.[/green]")
40
+
41
+
42
+ @app.command("to-json")
43
+ def to_json_cmd(
44
+ path: Path = typer.Argument(..., help="مسیر فایل TOML."),
45
+ output: Optional[Path] = typer.Option(None, "--output", "-o"),
46
+ indent: int = typer.Option(2, "--indent", "-i", min=0, max=10),
47
+ ) -> None:
48
+ """تبدیل TOML به JSON."""
49
+ if not path.exists():
50
+ _fail(f"فایل یافت نشد: {path}")
51
+ try:
52
+ data = parse_toml(read_text(path))
53
+ except ValueError as e:
54
+ _fail(str(e))
55
+ text = dump_json(data, indent=indent)
56
+ if output:
57
+ write_text(output, text)
58
+ console.print(f"[green]✓ ذخیره شد:[/green] {output}")
59
+ else:
60
+ console.print(text, highlight=False)
61
+
62
+
63
+ @app.command("from-json")
64
+ def from_json_cmd(
65
+ path: Path = typer.Argument(..., help="مسیر فایل JSON."),
66
+ output: Optional[Path] = typer.Option(None, "--output", "-o"),
67
+ ) -> None:
68
+ """تبدیل JSON به TOML."""
69
+ if not path.exists():
70
+ _fail(f"فایل یافت نشد: {path}")
71
+ try:
72
+ data = parse_json(read_text(path))
73
+ except ValueError as e:
74
+ _fail(str(e))
75
+ if not isinstance(data, dict):
76
+ _fail("ساختار سطح اول JSON باید object باشد.")
77
+ try:
78
+ text = dump_toml(data)
79
+ except ValueError as e:
80
+ _fail(str(e))
81
+ if output:
82
+ write_text(output, text)
83
+ console.print(f"[green]✓ ذخیره شد:[/green] {output}")
84
+ else:
85
+ console.print(text, highlight=False)
@@ -0,0 +1,102 @@
1
+ """دستور pdev url — کار با URL و query string."""
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.url_utils import (
12
+ build_query,
13
+ decode_query,
14
+ parse_url,
15
+ url_decode,
16
+ url_encode,
17
+ )
18
+
19
+ app = typer.Typer(help="کار با URL و query string.", no_args_is_help=True)
20
+ console = Console()
21
+
22
+
23
+ def _read(value: Optional[str]) -> str:
24
+ if value is not None and value != "":
25
+ return value
26
+ if not sys.stdin.isatty():
27
+ data = sys.stdin.read()
28
+ if data.strip():
29
+ return data.rstrip("\n")
30
+ console.print("[red]✗ خطا:[/red] ورودی‌ای داده نشده.")
31
+ raise typer.Exit(1)
32
+
33
+
34
+ @app.command("encode")
35
+ def encode_cmd(
36
+ text: Optional[str] = typer.Argument(None, help="متن ورودی (یا از stdin)."),
37
+ safe: str = typer.Option("", "--safe", help="کاراکترهای ایمن (رمزگذاری نمی‌شوند)."),
38
+ ) -> None:
39
+ """درصد-رمزگذاری متن."""
40
+ console.print(url_encode(_read(text), safe=safe))
41
+
42
+
43
+ @app.command("decode")
44
+ def decode_cmd(
45
+ text: Optional[str] = typer.Argument(None, help="رشتهٔ رمزگذاری‌شده (یا از stdin)."),
46
+ ) -> None:
47
+ """رمزگشایی متن درصد-رمزگذاری‌شده."""
48
+ console.print(url_decode(_read(text)))
49
+
50
+
51
+ @app.command("parse")
52
+ def parse_cmd(
53
+ url: str = typer.Argument(..., help="آدرس URL."),
54
+ ) -> None:
55
+ """تجزیهٔ URL به اجزای سازنده."""
56
+ result = parse_url(url)
57
+ table = Table(title="🔗 URL", title_style="bold cyan")
58
+ table.add_column("بخش", style="bold")
59
+ table.add_column("مقدار")
60
+
61
+ for key in ("scheme", "netloc", "host", "port", "path", "fragment", "username"):
62
+ value = result.get(key)
63
+ if value:
64
+ table.add_row(key, str(value))
65
+ if result["query"]:
66
+ for k, v in result["query"].items():
67
+ table.add_row(f"query.{k}", v)
68
+ console.print(table)
69
+
70
+
71
+ @app.command("build")
72
+ def build_cmd(
73
+ params: list[str] = typer.Argument(
74
+ ..., help="جفت‌های key=value برای ساخت query string."
75
+ ),
76
+ ) -> None:
77
+ """ساخت query string از جفت‌های key=value."""
78
+ data: dict[str, str] = {}
79
+ for item in params:
80
+ if "=" not in item:
81
+ console.print(f"[red]✗ خطا:[/red] فرمت نامعتبر: {item} (باید key=value باشد)")
82
+ raise typer.Exit(1)
83
+ k, v = item.split("=", 1)
84
+ data[k] = v
85
+ console.print(build_query(data))
86
+
87
+
88
+ @app.command("decode-query")
89
+ def decode_query_cmd(
90
+ query: str = typer.Argument(..., help="رشتهٔ query string."),
91
+ ) -> None:
92
+ """تجزیهٔ query string به کلید/مقدار."""
93
+ result = decode_query(query)
94
+ if not result:
95
+ console.print("[yellow]⚠ query string خالی است.[/yellow]")
96
+ return
97
+ table = Table(title="📋 Query", title_style="bold cyan")
98
+ table.add_column("کلید", style="bold")
99
+ table.add_column("مقدار")
100
+ for k, values in result.items():
101
+ table.add_row(k, ", ".join(values))
102
+ console.print(table)
@@ -0,0 +1,45 @@
1
+ """دستور `pdev uuid` — تولید UUID."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import uuid
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ app = typer.Typer(help="تولید UUID.", no_args_is_help=True)
11
+ console = Console()
12
+
13
+
14
+ def short_uuid() -> str:
15
+ """تولید UUID کوتاه بر پایهٔ Base64 (۲۲ کاراکتر)."""
16
+ raw = uuid.uuid4().bytes
17
+ return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
18
+
19
+
20
+ @app.command("new")
21
+ def new_cmd(
22
+ count: int = typer.Option(
23
+ 1, "--count", "-c", min=1, max=10_000, help="تعداد UUID برای تولید."
24
+ ),
25
+ upper: bool = typer.Option(
26
+ False, "--upper", "-u", help="خروجی با حروف بزرگ."
27
+ ),
28
+ ) -> None:
29
+ """تولید UUID نسخهٔ ۴."""
30
+ for _ in range(count):
31
+ value = str(uuid.uuid4())
32
+ if upper:
33
+ value = value.upper()
34
+ console.print(value)
35
+
36
+
37
+ @app.command("short")
38
+ def short_cmd(
39
+ count: int = typer.Option(
40
+ 1, "--count", "-c", min=1, max=10_000, help="تعداد UUID کوتاه."
41
+ ),
42
+ ) -> None:
43
+ """تولید UUID کوتاه (Base64، ۲۲ کاراکتر)."""
44
+ for _ in range(count):
45
+ console.print(short_uuid())
@@ -0,0 +1,94 @@
1
+ """دستور pdev yaml — کار با فایل‌های YAML."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any, Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from persian_devkit.utils.data_utils import (
11
+ dump_json,
12
+ dump_yaml,
13
+ parse_json,
14
+ parse_yaml,
15
+ read_text,
16
+ write_text,
17
+ )
18
+
19
+ app = typer.Typer(help="کار با فایل‌های YAML.", no_args_is_help=True)
20
+ console = Console()
21
+
22
+
23
+ def _fail(msg: str) -> None:
24
+ console.print(f"[red]✗ خطا:[/red] {msg}")
25
+ raise typer.Exit(1)
26
+
27
+
28
+ def _load(path: Path) -> Any:
29
+ if not path.exists():
30
+ _fail(f"فایل یافت نشد: {path}")
31
+ try:
32
+ return parse_yaml(read_text(path))
33
+ except ValueError as e:
34
+ _fail(str(e))
35
+
36
+
37
+ @app.command("pretty")
38
+ def pretty_cmd(
39
+ path: Path = typer.Argument(..., help="مسیر فایل YAML."),
40
+ output: Optional[Path] = typer.Option(None, "--output", "-o"),
41
+ ) -> None:
42
+ """زیباسازی فایل YAML."""
43
+ data = _load(path)
44
+ text = dump_yaml(data)
45
+ if output:
46
+ write_text(output, text)
47
+ console.print(f"[green]✓ ذخیره شد:[/green] {output}")
48
+ else:
49
+ console.print(text, highlight=False)
50
+
51
+
52
+ @app.command("validate")
53
+ def validate_cmd(
54
+ path: Path = typer.Argument(..., help="مسیر فایل YAML."),
55
+ ) -> None:
56
+ """بررسی صحت YAML."""
57
+ _load(path)
58
+ console.print("[green]✓ YAML معتبر است.[/green]")
59
+
60
+
61
+ @app.command("to-json")
62
+ def to_json_cmd(
63
+ path: Path = typer.Argument(..., help="مسیر فایل YAML."),
64
+ output: Optional[Path] = typer.Option(None, "--output", "-o"),
65
+ indent: int = typer.Option(2, "--indent", "-i", min=0, max=10),
66
+ ) -> None:
67
+ """تبدیل YAML به JSON."""
68
+ data = _load(path)
69
+ text = dump_json(data, indent=indent)
70
+ if output:
71
+ write_text(output, text)
72
+ console.print(f"[green]✓ ذخیره شد:[/green] {output}")
73
+ else:
74
+ console.print(text, highlight=False)
75
+
76
+
77
+ @app.command("from-json")
78
+ def from_json_cmd(
79
+ path: Path = typer.Argument(..., help="مسیر فایل JSON."),
80
+ output: Optional[Path] = typer.Option(None, "--output", "-o"),
81
+ ) -> None:
82
+ """تبدیل JSON به YAML."""
83
+ if not path.exists():
84
+ _fail(f"فایل یافت نشد: {path}")
85
+ try:
86
+ data = parse_json(read_text(path))
87
+ except ValueError as e:
88
+ _fail(str(e))
89
+ text = dump_yaml(data)
90
+ if output:
91
+ write_text(output, text)
92
+ console.print(f"[green]✓ ذخیره شد:[/green] {output}")
93
+ else:
94
+ console.print(text, highlight=False)
persian_devkit/main.py ADDED
@@ -0,0 +1,63 @@
1
+ """نقطهٔ ورود اصلی ابزار pdev."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ # اصلاح encoding در ویندوز
7
+ if sys.platform == "win32":
8
+ for _stream_name in ("stdin", "stdout", "stderr"):
9
+ _stream = getattr(sys, _stream_name, None)
10
+ if _stream is not None and hasattr(_stream, "reconfigure"):
11
+ try:
12
+ _stream.reconfigure(encoding="utf-8")
13
+ except Exception:
14
+ pass
15
+
16
+ import typer
17
+ from persian_devkit import __version__
18
+ from persian_devkit.cli import register_commands
19
+
20
+ app = typer.Typer(
21
+ name="pdev",
22
+ help="جعبه‌ابزار خط فرمان برای توسعه‌دهندگان فارسی‌زبان.",
23
+ no_args_is_help=True,
24
+ add_completion=True,
25
+ rich_markup_mode="rich",
26
+ )
27
+
28
+
29
+ def _version_callback(value: bool) -> None:
30
+ """نمایش نسخه و خروج فوری."""
31
+ if value:
32
+ typer.echo(f"persian-devkit {__version__}")
33
+ raise typer.Exit()
34
+
35
+
36
+ @app.callback()
37
+ def main_callback(
38
+ ctx: typer.Context,
39
+ version: bool = typer.Option(
40
+ False,
41
+ "--version",
42
+ "-V",
43
+ help="نمایش نسخهٔ ابزار و خروج.",
44
+ callback=_version_callback,
45
+ is_eager=True,
46
+ ),
47
+ verbose: bool = typer.Option(
48
+ False, "--verbose", "-v", help="نمایش جزئیات بیشتر در خروجی."
49
+ ),
50
+ quiet: bool = typer.Option(
51
+ False, "--quiet", "-q", help="کاهش پیام‌های اضافی (فقط نتیجه)."
52
+ ),
53
+ ) -> None:
54
+ """گزینه‌های سراسری pdev."""
55
+ ctx.obj = {"verbose": verbose, "quiet": quiet}
56
+
57
+
58
+ # ثبت زیرفرمان‌ها
59
+ register_commands(app)
60
+
61
+
62
+ if __name__ == "__main__": # pragma: no cover
63
+ app()
File without changes
@@ -0,0 +1 @@
1
+ """توابع کمکی قابل استفادهٔ مجدد."""
@@ -0,0 +1,112 @@
1
+ """تبدیل و پردازش رنگ‌ها."""
2
+ from __future__ import annotations
3
+
4
+ import colorsys
5
+ import re
6
+ import secrets
7
+
8
+ _HEX_RE = re.compile(r"^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
9
+ _RGB_FUNC_RE = re.compile(r"^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)")
10
+ _RGB_TRIPLE_RE = re.compile(r"^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$")
11
+
12
+ RGBTuple = tuple[int, int, int]
13
+
14
+
15
+ def parse_color(value: str) -> RGBTuple:
16
+ """تشخیص خودکار فرمت رنگ و برگرداندن (r, g, b).
17
+
18
+ فرمت‌های پشتیبانی‌شده: #fff, #ffffff, ff0000, rgb(255,0,0), 255,0,0
19
+ """
20
+ v = value.strip()
21
+
22
+ m = _HEX_RE.match(v)
23
+ if m:
24
+ h = m.group(1)
25
+ if len(h) == 3:
26
+ h = "".join(c * 2 for c in h)
27
+ return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
28
+
29
+ m = _RGB_FUNC_RE.match(v)
30
+ if m:
31
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
32
+
33
+ m = _RGB_TRIPLE_RE.match(v)
34
+ if m:
35
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
36
+
37
+ raise ValueError(f"فرمت رنگ نامعتبر: {value}")
38
+
39
+
40
+ def rgb_to_hex(rgb: RGBTuple) -> str:
41
+ """تبدیل (r, g, b) به #rrggbb."""
42
+ return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
43
+
44
+
45
+ def hex_to_rgb(hex_str: str) -> RGBTuple:
46
+ """تبدیل #rrggbb یا #rgb به (r, g, b)."""
47
+ return parse_color(hex_str)
48
+
49
+
50
+ def rgb_to_hsl(rgb: RGBTuple) -> tuple[float, float, float]:
51
+ """تبدیل RGB به HSL (h درجه ۰-۳۶۰، s و l در بازهٔ ۰-۱)."""
52
+ r, g, b = (c / 255 for c in rgb)
53
+ h, l, s = colorsys.rgb_to_hls(r, g, b)
54
+ return (h * 360, s, l)
55
+
56
+
57
+ def hsl_to_rgb(h: float, s: float, l: float) -> RGBTuple:
58
+ """تبدیل HSL به RGB."""
59
+ r, g, b = colorsys.hls_to_rgb(h / 360, l, s)
60
+ return (round(r * 255), round(g * 255), round(b * 255))
61
+
62
+
63
+ def random_color() -> RGBTuple:
64
+ """رنگ کاملاً تصادفی."""
65
+ return (secrets.randbelow(256), secrets.randbelow(256), secrets.randbelow(256))
66
+
67
+
68
+ def pleasant_color() -> RGBTuple:
69
+ """رنگ تصادفی با اشباع و روشنایی دلپذیر."""
70
+ h = secrets.randbelow(360)
71
+ s = 0.5 + secrets.randbelow(40) / 100
72
+ l = 0.4 + secrets.randbelow(30) / 100
73
+ return hsl_to_rgb(h, s, l)
74
+
75
+
76
+ def generate_palette(rgb: RGBTuple, count: int = 5) -> list[RGBTuple]:
77
+ """تولید پالت از رنگ پایه (از تیره به روشن)."""
78
+ if count < 2:
79
+ return [rgb]
80
+ h, s, _ = rgb_to_hsl(rgb)
81
+ palette: list[RGBTuple] = []
82
+ for i in range(count):
83
+ new_l = 0.15 + (0.70 * i / (count - 1))
84
+ palette.append(hsl_to_rgb(h, s, new_l))
85
+ return palette
86
+
87
+
88
+ def complementary(rgb: RGBTuple) -> RGBTuple:
89
+ """رنگ مکمل (۱۸۰ درجه چرخش hue)."""
90
+ h, s, l = rgb_to_hsl(rgb)
91
+ return hsl_to_rgb((h + 180) % 360, s, l)
92
+
93
+
94
+ def luminance(rgb: RGBTuple) -> float:
95
+ """روشنایی نسبی (۰-۱) طبق فرمول WCAG."""
96
+ def channel(c: int) -> float:
97
+ v = c / 255
98
+ return v / 12.92 if v <= 0.03928 else ((v + 0.055) / 1.055) ** 2.4
99
+
100
+ return 0.2126 * channel(rgb[0]) + 0.7152 * channel(rgb[1]) + 0.0722 * channel(rgb[2])
101
+
102
+
103
+ def contrast_ratio(a: RGBTuple, b: RGBTuple) -> float:
104
+ """نسبت کنتراست WCAG بین دو رنگ (۱ تا ۲۱)."""
105
+ la, lb = luminance(a), luminance(b)
106
+ lighter, darker = max(la, lb), min(la, lb)
107
+ return (lighter + 0.05) / (darker + 0.05)
108
+
109
+
110
+ def best_text_color(bg: RGBTuple) -> str:
111
+ """بهترین رنگ متن (سیاه یا سفید) برای پس‌زمینه."""
112
+ return "black" if luminance(bg) > 0.5 else "white"