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,211 @@
1
+ """قالب‌های `.gitignore` برای زبان‌ها و ابزارهای رایج."""
2
+
3
+ GITIGNORES: dict[str, str] = {
4
+ "python": """# Python
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ *.so
9
+ .Python
10
+ build/
11
+ develop-eggs/
12
+ dist/
13
+ downloads/
14
+ eggs/
15
+ .eggs/
16
+ lib/
17
+ lib64/
18
+ parts/
19
+ sdist/
20
+ var/
21
+ wheels/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+ MANIFEST
26
+
27
+ # Virtualenv
28
+ .venv
29
+ venv/
30
+ ENV/
31
+ env/
32
+
33
+ # Testing
34
+ .pytest_cache/
35
+ .coverage
36
+ htmlcov/
37
+ .tox/
38
+ .mypy_cache/
39
+ .ruff_cache/
40
+
41
+ # Distribution
42
+ *.whl
43
+
44
+ # IDE
45
+ .idea/
46
+ .vscode/
47
+ """,
48
+ "node": """# Node
49
+ node_modules/
50
+ npm-debug.log*
51
+ yarn-debug.log*
52
+ yarn-error.log*
53
+ lerna-debug.log*
54
+ .pnpm-debug.log*
55
+
56
+ # Build
57
+ dist/
58
+ build/
59
+ .next/
60
+ .nuxt/
61
+ out/
62
+
63
+ # Cache
64
+ .cache/
65
+ .parcel-cache/
66
+ .eslintcache
67
+
68
+ # Env
69
+ .env
70
+ .env.local
71
+ .env.*.local
72
+
73
+ # IDE
74
+ .idea/
75
+ .vscode/
76
+ """,
77
+ "go": """# Go
78
+ *.exe
79
+ *.exe~
80
+ *.dll
81
+ *.so
82
+ *.dylib
83
+ *.test
84
+ *.out
85
+
86
+ # Vendor
87
+ vendor/
88
+
89
+ # Binary
90
+ /bin/
91
+ main
92
+ """,
93
+ "rust": """# Rust
94
+ /target/
95
+ **/*.rs.bk
96
+ Cargo.lock
97
+
98
+ # Debug
99
+ *.pdb
100
+ """,
101
+ "java": """# Java
102
+ *.class
103
+ *.jar
104
+ *.war
105
+ *.ear
106
+ *.nar
107
+ hs_err_pid*
108
+ replay_pid*
109
+
110
+ # Maven
111
+ target/
112
+
113
+ # Gradle
114
+ .gradle/
115
+ build/
116
+
117
+ # IDE
118
+ .idea/
119
+ *.iml
120
+ """,
121
+ "cpp": """# C/C++
122
+ *.o
123
+ *.obj
124
+ *.exe
125
+ *.out
126
+ *.app
127
+ *.so
128
+ *.dylib
129
+ *.dll
130
+
131
+ # Build directories
132
+ build/
133
+ cmake-build-*/
134
+
135
+ # Debug
136
+ *.dSYM/
137
+ """,
138
+ "macos": """# macOS
139
+ .DS_Store
140
+ .AppleDouble
141
+ .LSOverride
142
+ ._*
143
+
144
+ # Thumbnails
145
+ **/.Spotlight-V100
146
+ **/.Trashes
147
+ **/.fseventsd
148
+ """,
149
+ "windows": """# Windows
150
+ Thumbs.db
151
+ Thumbs.db:encryptable
152
+ ehthumbs.db
153
+ ehthumbs_vista.db
154
+ Desktop.ini
155
+ $RECYCLE.BIN/
156
+ *.lnk
157
+ """,
158
+ "linux": """# Linux
159
+ *~
160
+ .fuse_hidden*
161
+ .directory
162
+ .Trash-*
163
+ .nfs*
164
+ """,
165
+ "vscode": """# VS Code
166
+ .vscode/*
167
+ !.vscode/settings.json
168
+ !.vscode/tasks.json
169
+ !.vscode/launch.json
170
+ !.vscode/extensions.json
171
+ !.vscode/*.code-snippets
172
+ .history/
173
+ *.vsix
174
+ """,
175
+ "jetbrains": """# JetBrains IDEs
176
+ .idea/
177
+ *.iml
178
+ *.iws
179
+ *.ipr
180
+ out/
181
+ """,
182
+ "vim": """# Vim
183
+ [._]*.s[a-v][a-z]
184
+ [._]*.sw[a-p]
185
+ [._]s[a-rt-v][a-z]
186
+ [._]ss[a-gi-z]
187
+ [._]sw[a-p]
188
+ Session.vim
189
+ .netrwhist
190
+ *~
191
+ """,
192
+ }
193
+
194
+
195
+ def combine_gitignores(langs: list[str]) -> str:
196
+ """ترکیب چند .gitignore با جداکننده."""
197
+ parts: list[str] = []
198
+ for lang in langs:
199
+ key = lang.lower()
200
+ if key not in GITIGNORES:
201
+ raise ValueError(
202
+ f"زبان ناشناخته: {lang}. "
203
+ f"موارد مجاز: {', '.join(GITIGNORES.keys())}"
204
+ )
205
+ parts.append(GITIGNORES[key].rstrip())
206
+ return "\n\n".join(parts) + "\n"
207
+
208
+
209
+ def list_gitignores() -> list[str]:
210
+ """لیست همهٔ زبان‌ها و ابزارها."""
211
+ return sorted(GITIGNORES.keys())
@@ -0,0 +1,37 @@
1
+ """اطلاعات و تبدیل تصاویر."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from PIL import Image
7
+
8
+
9
+ def image_info(path: Path) -> dict:
10
+ """اطلاعات فایل تصویری: فرمت، حالت، ابعاد، اندازه."""
11
+ with Image.open(path) as img:
12
+ return {
13
+ "format": img.format or "unknown",
14
+ "mode": img.mode,
15
+ "width": img.width,
16
+ "height": img.height,
17
+ "size_bytes": path.stat().st_size,
18
+ }
19
+
20
+
21
+ def resize_image(src: Path, dst: Path, width: int, height: int | None = None) -> None:
22
+ """تغییر اندازهٔ تصویر (اگر height خالی باشد، نسبت حفظ می‌شود)."""
23
+ with Image.open(src) as img:
24
+ if height is None:
25
+ ratio = width / img.width
26
+ height = max(1, round(img.height * ratio))
27
+ resized = img.resize((width, height), Image.LANCZOS)
28
+ resized.save(dst)
29
+
30
+
31
+ def convert_image(src: Path, dst: Path, quality: int = 90) -> None:
32
+ """تبدیل فرمت تصویر بر اساس پسوند مقصد."""
33
+ with Image.open(src) as img:
34
+ if dst.suffix.lower() in (".jpg", ".jpeg"):
35
+ img.convert("RGB").save(dst, quality=quality)
36
+ else:
37
+ img.save(dst)
@@ -0,0 +1,94 @@
1
+ """پیاده‌سازی سادهٔ JSONPath با پشتیبانی از dot و [index] و [*]."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from typing import Any
6
+
7
+
8
+ class JSONPathError(ValueError):
9
+ """خطا در مسیر JSONPath."""
10
+
11
+
12
+ # توکن‌ها: .key یا [0] یا ["key"] یا [*] یا key در ابتدای مسیر
13
+ _TOKEN_RE = re.compile(
14
+ r"""
15
+ \.([A-Za-z_][\w\-]*) # .key
16
+ | \[\s*(\d+)\s*\] # [0]
17
+ | \[\s*['"]([^'"]+)['"]\s*\] # ["key"]
18
+ | \[\s*(\*)\s*\] # [*]
19
+ | ^([A-Za-z_][\w\-]*) # key در ابتدای مسیر (بدون نقطه)
20
+ """,
21
+ re.VERBOSE,
22
+ )
23
+
24
+
25
+ def _tokenize(path: str) -> list[tuple[str, Any]]:
26
+ """مسیر را به توکن‌های (نوع, مقدار) تجزیه می‌کند."""
27
+ p = path.strip()
28
+ if p == "" or p == "$":
29
+ return []
30
+
31
+ if p.startswith("$"):
32
+ p = p[1:]
33
+
34
+ tokens: list[tuple[str, Any]] = []
35
+ pos = 0
36
+ while pos < len(p):
37
+ m = _TOKEN_RE.match(p, pos)
38
+ if not m:
39
+ raise JSONPathError(f"مسیر نامعتبر در موقعیت {pos}: {p[pos:pos+20]!r}")
40
+ key1, idx, key2, wildcard, key3 = m.groups()
41
+ if key1 is not None:
42
+ tokens.append(("key", key1))
43
+ elif idx is not None:
44
+ tokens.append(("index", int(idx)))
45
+ elif key2 is not None:
46
+ tokens.append(("key", key2))
47
+ elif wildcard is not None:
48
+ tokens.append(("wildcard", None))
49
+ elif key3 is not None:
50
+ tokens.append(("key", key3))
51
+ pos = m.end()
52
+ return tokens
53
+
54
+
55
+ def query(data: Any, path: str) -> Any:
56
+ """اجرای مسیر روی داده و برگرداندن مقدار.
57
+
58
+ مسیرهای پشتیبانی‌شده:
59
+ .user.name → کلید تودرتو
60
+ .items[0] → اندیس آرایه
61
+ .items[*].name → همهٔ عناصر
62
+ ["key with space"] → کلید با کاراکتر خاص
63
+ $ → خود داده
64
+ """
65
+ tokens = _tokenize(path)
66
+ results: list[Any] = [data]
67
+
68
+ for kind, value in tokens:
69
+ next_results: list[Any] = []
70
+ for item in results:
71
+ if kind == "key":
72
+ if isinstance(item, dict) and value in item:
73
+ next_results.append(item[value])
74
+ elif kind == "index":
75
+ if isinstance(item, list) and -len(item) <= value < len(item):
76
+ next_results.append(item[value])
77
+ elif kind == "wildcard":
78
+ if isinstance(item, list):
79
+ next_results.extend(item)
80
+ elif isinstance(item, dict):
81
+ next_results.extend(item.values())
82
+ results = next_results
83
+
84
+ if len(results) == 1:
85
+ return results[0]
86
+ return results
87
+
88
+
89
+ def query_many(data: Any, path: str) -> list[Any]:
90
+ """مثل query اما همیشه لیست برمی‌گرداند."""
91
+ result = query(data, path)
92
+ if isinstance(result, list):
93
+ return result
94
+ return [result]
@@ -0,0 +1,190 @@
1
+ """قالب‌های متن لایسنس‌های متن‌باز رایج."""
2
+
3
+ LICENSES: dict[str, dict[str, str]] = {
4
+ "MIT": {
5
+ "name": "MIT License",
6
+ "spdx": "MIT",
7
+ "template": """MIT License
8
+
9
+ Copyright (c) {year} {author}
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ """,
29
+ },
30
+ "Apache-2.0": {
31
+ "name": "Apache License 2.0",
32
+ "spdx": "Apache-2.0",
33
+ "template": """ Apache License
34
+ Version 2.0, January 2004
35
+ http://www.apache.org/licenses/
36
+
37
+ Copyright {year} {author}
38
+
39
+ Licensed under the Apache License, Version 2.0 (the "License");
40
+ you may not use this file except in compliance with the License.
41
+ You may obtain a copy of the License at
42
+
43
+ http://www.apache.org/licenses/LICENSE-2.0
44
+
45
+ Unless required by applicable law or agreed to in writing, software
46
+ distributed under the License is distributed on an "AS IS" BASIS,
47
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
48
+ See the License for the specific language governing permissions and
49
+ limitations under the License.
50
+
51
+ For the full license text, see:
52
+ https://www.apache.org/licenses/LICENSE-2.0.txt
53
+ """,
54
+ },
55
+ "BSD-3-Clause": {
56
+ "name": "BSD 3-Clause \"New\" or \"Revised\" License",
57
+ "spdx": "BSD-3-Clause",
58
+ "template": """BSD 3-Clause License
59
+
60
+ Copyright (c) {year}, {author}
61
+ All rights reserved.
62
+
63
+ Redistribution and use in source and binary forms, with or without
64
+ modification, are permitted provided that the following conditions are met:
65
+
66
+ 1. Redistributions of source code must retain the above copyright notice, this
67
+ list of conditions and the following disclaimer.
68
+
69
+ 2. Redistributions in binary form must reproduce the above copyright notice,
70
+ this list of conditions and the following disclaimer in the documentation
71
+ and/or other materials provided with the distribution.
72
+
73
+ 3. Neither the name of the copyright holder nor the names of its
74
+ contributors may be used to endorse or promote products derived from
75
+ this software without specific prior written permission.
76
+
77
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
78
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
79
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
80
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
81
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
82
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
83
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
84
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
85
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
86
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
87
+ """,
88
+ },
89
+ "GPL-3.0": {
90
+ "name": "GNU General Public License v3.0",
91
+ "spdx": "GPL-3.0",
92
+ "template": """GNU GENERAL PUBLIC LICENSE
93
+ Version 3, 29 June 2007
94
+
95
+ Copyright (C) {year} {author}
96
+
97
+ This program is free software: you can redistribute it and/or modify
98
+ it under the terms of the GNU General Public License as published by
99
+ the Free Software Foundation, either version 3 of the License, or
100
+ (at your option) any later version.
101
+
102
+ This program is distributed in the hope that it will be useful,
103
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
104
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
105
+ GNU General Public License for more details.
106
+
107
+ You should have received a copy of the GNU General Public License
108
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
109
+
110
+ Full license text: https://www.gnu.org/licenses/gpl-3.0.txt
111
+ """,
112
+ },
113
+ "MPL-2.0": {
114
+ "name": "Mozilla Public License 2.0",
115
+ "spdx": "MPL-2.0",
116
+ "template": """Mozilla Public License Version 2.0
117
+ ==================================
118
+
119
+ Copyright (c) {year} {author}
120
+
121
+ This Source Code Form is subject to the terms of the Mozilla Public
122
+ License, v. 2.0. If a copy of the MPL was not distributed with this
123
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
124
+
125
+ Full license text: https://www.mozilla.org/MPL/2.0/
126
+ """,
127
+ },
128
+ "ISC": {
129
+ "name": "ISC License",
130
+ "spdx": "ISC",
131
+ "template": """ISC License
132
+
133
+ Copyright (c) {year} {author}
134
+
135
+ Permission to use, copy, modify, and/or distribute this software for any
136
+ purpose with or without fee is hereby granted, provided that the above
137
+ copyright notice and this permission notice appear in all copies.
138
+
139
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
140
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
141
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
142
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
143
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
144
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
145
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
146
+ """,
147
+ },
148
+ "Unlicense": {
149
+ "name": "The Unlicense",
150
+ "spdx": "Unlicense",
151
+ "template": """This is free and unencumbered software released into the public domain.
152
+
153
+ Anyone is free to copy, modify, publish, use, compile, sell, or distribute
154
+ this software, either in source code form or as a compiled binary, for any
155
+ purpose, commercial or non-commercial, and by any means.
156
+
157
+ In jurisdictions that recognize copyright laws, the author or authors of
158
+ this software dedicate any and all copyright interest in the software to
159
+ the public domain. We make this dedication for the benefit of the public
160
+ at large and to the detriment of our heirs and successors.
161
+
162
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
163
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
164
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
165
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
166
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
167
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
168
+
169
+ For more information, please refer to <https://unlicense.org>
170
+ """,
171
+ },
172
+ }
173
+
174
+
175
+ def render_license(spdx: str, author: str, year: int | str) -> str:
176
+ """رندر متن لایسنس با جایگذاری نویسنده و سال."""
177
+ if spdx not in LICENSES:
178
+ raise ValueError(
179
+ f"لایسنس ناشناخته: {spdx}. "
180
+ f"موارد مجاز: {', '.join(LICENSES.keys())}"
181
+ )
182
+ return LICENSES[spdx]["template"].format(author=author, year=year)
183
+
184
+
185
+ def list_licenses() -> list[dict[str, str]]:
186
+ """لیست همهٔ لایسنس‌های پشتیبانی‌شده."""
187
+ return [
188
+ {"spdx": k, "name": v["name"]}
189
+ for k, v in LICENSES.items()
190
+ ]
@@ -0,0 +1,78 @@
1
+ """تبدیل و قالب‌بندی اعداد با پشتیبانی فارسی."""
2
+ from __future__ import annotations
3
+
4
+ PERSIAN_DIGITS = "۰۱۲۳۴۵۶۷۸۹"
5
+ ENGLISH_DIGITS = "0123456789"
6
+ ARABIC_DIGITS = "٠١٢٣٤٥٦٧٨٩"
7
+
8
+ _ONES = ["", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه"]
9
+ _TEENS = [
10
+ "ده", "یازده", "دوازده", "سیزده", "چهارده",
11
+ "پانزده", "شانزده", "هفده", "هجده", "نوزده",
12
+ ]
13
+ _TENS = ["", "", "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود"]
14
+ _HUNDREDS = ["", "صد", "دویست", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد", "نهصد"]
15
+ _SCALES = ["", " هزار", " میلیون", " میلیارد", " بیلیون"]
16
+
17
+
18
+ def to_persian_digits(value: str) -> str:
19
+ """تبدیل ارقام لاتین/عربی به ارقام فارسی."""
20
+ table = str.maketrans(
21
+ ENGLISH_DIGITS + ARABIC_DIGITS,
22
+ PERSIAN_DIGITS + PERSIAN_DIGITS,
23
+ )
24
+ return value.translate(table)
25
+
26
+
27
+ def to_english_digits(value: str) -> str:
28
+ """تبدیل ارقام فارسی/عربی به ارقام لاتین."""
29
+ table = str.maketrans(
30
+ PERSIAN_DIGITS + ARABIC_DIGITS,
31
+ ENGLISH_DIGITS + ENGLISH_DIGITS,
32
+ )
33
+ return value.translate(table)
34
+
35
+
36
+ def format_thousands(n: int) -> str:
37
+ """قالب‌بندی عدد با جداکنندهٔ هزارگان."""
38
+ return f"{n:,}"
39
+
40
+
41
+ def _three_digit_to_words(n: int) -> str:
42
+ """تبدیل عدد سه‌رقمی (۰ تا ۹۹۹) به حروف فارسی."""
43
+ parts: list[str] = []
44
+ h, rem = divmod(n, 100)
45
+ if h:
46
+ parts.append(_HUNDREDS[h])
47
+ if 10 <= rem < 20:
48
+ parts.append(_TEENS[rem - 10])
49
+ else:
50
+ t, o = divmod(rem, 10)
51
+ if t:
52
+ parts.append(_TENS[t])
53
+ if o:
54
+ parts.append(_ONES[o])
55
+ return " و ".join(parts)
56
+
57
+
58
+ def number_to_words(n: int) -> str:
59
+ """تبدیل عدد صحیح به معادل نوشتاری فارسی."""
60
+ if n == 0:
61
+ return "صفر"
62
+ if n < 0:
63
+ return "منفی " + number_to_words(-n)
64
+
65
+ groups: list[int] = []
66
+ value = n
67
+ while value > 0:
68
+ groups.append(value % 1000)
69
+ value //= 1000
70
+
71
+ parts: list[str] = []
72
+ for idx in range(len(groups) - 1, -1, -1):
73
+ group = groups[idx]
74
+ if group == 0:
75
+ continue
76
+ text = _three_digit_to_words(group)
77
+ parts.append(text + _SCALES[idx])
78
+ return " و ".join(parts)
@@ -0,0 +1,84 @@
1
+ """تولید QR Code برای چاپ در ترمینال یا ذخیره در فایل."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ import qrcode
7
+ from qrcode.constants import (
8
+ ERROR_CORRECT_H,
9
+ ERROR_CORRECT_L,
10
+ ERROR_CORRECT_M,
11
+ ERROR_CORRECT_Q,
12
+ )
13
+
14
+ _ERROR_LEVELS = {
15
+ "L": ERROR_CORRECT_L,
16
+ "M": ERROR_CORRECT_M,
17
+ "Q": ERROR_CORRECT_Q,
18
+ "H": ERROR_CORRECT_H,
19
+ }
20
+
21
+
22
+ def _make_qr(text: str, error: str = "M", box_size: int = 10, border: int = 2):
23
+ """شیء QR را با تنظیمات داده‌شده می‌سازد."""
24
+ if error not in _ERROR_LEVELS:
25
+ raise ValueError(
26
+ f"سطح تصحیح خطا نامعتبر: {error}. مجاز: L, M, Q, H"
27
+ )
28
+ qr = qrcode.QRCode(
29
+ version=None,
30
+ error_correction=_ERROR_LEVELS[error],
31
+ box_size=box_size,
32
+ border=border,
33
+ )
34
+ qr.add_data(text)
35
+ qr.make(fit=True)
36
+ return qr
37
+
38
+
39
+ def qr_to_ascii(text: str, error: str = "M", border: int = 1, invert: bool = True) -> str:
40
+ """تولید نمایش ASCII از QR برای چاپ در ترمینال.
41
+
42
+ با ``invert=True`` (پیش‌فرض)، ماژول‌های تیره به‌عنوان پس‌زمینه نمایش داده می‌شوند
43
+ که برای ترمینال‌های تیره مناسب است.
44
+ """
45
+ qr = _make_qr(text, error=error, box_size=1, border=border)
46
+ matrix = qr.get_matrix()
47
+ rows = len(matrix)
48
+ cols = len(matrix[0]) if rows else 0
49
+
50
+ def render(top: bool, bottom: bool) -> str:
51
+ if invert:
52
+ top, bottom = not top, not bottom
53
+ if top and bottom:
54
+ return "█"
55
+ if top:
56
+ return "▀"
57
+ if bottom:
58
+ return "▄"
59
+ return " "
60
+
61
+ lines: list[str] = []
62
+ for y in range(0, rows, 2):
63
+ chars = []
64
+ for x in range(cols):
65
+ top = matrix[y][x]
66
+ bottom = matrix[y + 1][x] if y + 1 < rows else False
67
+ chars.append(render(top, bottom))
68
+ lines.append("".join(chars))
69
+ return "\n".join(lines)
70
+
71
+
72
+ def qr_to_image(
73
+ text: str,
74
+ output: Path,
75
+ error: str = "M",
76
+ box_size: int = 10,
77
+ border: int = 4,
78
+ fill_color: str = "black",
79
+ back_color: str = "white",
80
+ ) -> None:
81
+ """ذخیرهٔ QR به‌صورت فایل تصویری."""
82
+ qr = _make_qr(text, error=error, box_size=box_size, border=border)
83
+ img = qr.make_image(fill_color=fill_color, back_color=back_color)
84
+ img.save(str(output))