absfuyu 5.0.0__py3-none-any.whl → 6.1.2__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.

Potentially problematic release.


This version of absfuyu might be problematic. Click here for more details.

Files changed (103) hide show
  1. absfuyu/__init__.py +5 -3
  2. absfuyu/__main__.py +3 -3
  3. absfuyu/cli/__init__.py +13 -2
  4. absfuyu/cli/audio_group.py +98 -0
  5. absfuyu/cli/color.py +30 -14
  6. absfuyu/cli/config_group.py +9 -2
  7. absfuyu/cli/do_group.py +23 -6
  8. absfuyu/cli/game_group.py +27 -2
  9. absfuyu/cli/tool_group.py +81 -11
  10. absfuyu/config/__init__.py +3 -3
  11. absfuyu/core/__init__.py +12 -8
  12. absfuyu/core/baseclass.py +929 -96
  13. absfuyu/core/baseclass2.py +44 -3
  14. absfuyu/core/decorator.py +70 -4
  15. absfuyu/core/docstring.py +64 -41
  16. absfuyu/core/dummy_cli.py +3 -3
  17. absfuyu/core/dummy_func.py +19 -6
  18. absfuyu/dxt/__init__.py +2 -2
  19. absfuyu/dxt/base_type.py +93 -0
  20. absfuyu/dxt/dictext.py +204 -16
  21. absfuyu/dxt/dxt_support.py +2 -2
  22. absfuyu/dxt/intext.py +151 -34
  23. absfuyu/dxt/listext.py +969 -127
  24. absfuyu/dxt/strext.py +77 -17
  25. absfuyu/extra/__init__.py +2 -2
  26. absfuyu/extra/audio/__init__.py +8 -0
  27. absfuyu/extra/audio/_util.py +57 -0
  28. absfuyu/extra/audio/convert.py +192 -0
  29. absfuyu/extra/audio/lossless.py +281 -0
  30. absfuyu/extra/beautiful.py +3 -2
  31. absfuyu/extra/da/__init__.py +72 -0
  32. absfuyu/extra/da/dadf.py +1600 -0
  33. absfuyu/extra/da/dadf_base.py +186 -0
  34. absfuyu/extra/da/df_func.py +181 -0
  35. absfuyu/extra/da/mplt.py +219 -0
  36. absfuyu/extra/ggapi/__init__.py +8 -0
  37. absfuyu/extra/ggapi/gdrive.py +223 -0
  38. absfuyu/extra/ggapi/glicense.py +148 -0
  39. absfuyu/extra/ggapi/glicense_df.py +186 -0
  40. absfuyu/extra/ggapi/gsheet.py +88 -0
  41. absfuyu/extra/img/__init__.py +30 -0
  42. absfuyu/extra/img/converter.py +402 -0
  43. absfuyu/extra/img/dup_check.py +291 -0
  44. absfuyu/extra/pdf.py +87 -0
  45. absfuyu/extra/rclone.py +253 -0
  46. absfuyu/extra/xml.py +90 -0
  47. absfuyu/fun/__init__.py +7 -20
  48. absfuyu/fun/rubik.py +442 -0
  49. absfuyu/fun/tarot.py +2 -2
  50. absfuyu/game/__init__.py +2 -2
  51. absfuyu/game/game_stat.py +2 -2
  52. absfuyu/game/schulte.py +78 -0
  53. absfuyu/game/sudoku.py +2 -2
  54. absfuyu/game/tictactoe.py +2 -3
  55. absfuyu/game/wordle.py +6 -4
  56. absfuyu/general/__init__.py +4 -4
  57. absfuyu/general/content.py +4 -4
  58. absfuyu/general/human.py +2 -2
  59. absfuyu/general/resrel.py +213 -0
  60. absfuyu/general/shape.py +3 -8
  61. absfuyu/general/tax.py +344 -0
  62. absfuyu/logger.py +806 -59
  63. absfuyu/numbers/__init__.py +13 -0
  64. absfuyu/numbers/number_to_word.py +321 -0
  65. absfuyu/numbers/shorten_number.py +303 -0
  66. absfuyu/numbers/time_duration.py +217 -0
  67. absfuyu/pkg_data/__init__.py +2 -2
  68. absfuyu/pkg_data/deprecated.py +2 -2
  69. absfuyu/pkg_data/logo.py +1462 -0
  70. absfuyu/sort.py +4 -4
  71. absfuyu/tools/__init__.py +28 -2
  72. absfuyu/tools/checksum.py +144 -9
  73. absfuyu/tools/converter.py +120 -34
  74. absfuyu/tools/generator.py +461 -0
  75. absfuyu/tools/inspector.py +752 -0
  76. absfuyu/tools/keygen.py +2 -2
  77. absfuyu/tools/obfuscator.py +47 -9
  78. absfuyu/tools/passwordlib.py +89 -25
  79. absfuyu/tools/shutdownizer.py +3 -8
  80. absfuyu/tools/sw.py +718 -0
  81. absfuyu/tools/web.py +10 -13
  82. absfuyu/typings.py +138 -0
  83. absfuyu/util/__init__.py +114 -6
  84. absfuyu/util/api.py +41 -18
  85. absfuyu/util/cli.py +119 -0
  86. absfuyu/util/gui.py +91 -0
  87. absfuyu/util/json_method.py +43 -14
  88. absfuyu/util/lunar.py +2 -2
  89. absfuyu/util/package.py +124 -0
  90. absfuyu/util/path.py +702 -82
  91. absfuyu/util/performance.py +122 -7
  92. absfuyu/util/shorten_number.py +244 -21
  93. absfuyu/util/text_table.py +481 -0
  94. absfuyu/util/zipped.py +8 -7
  95. absfuyu/version.py +79 -59
  96. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/METADATA +52 -11
  97. absfuyu-6.1.2.dist-info/RECORD +105 -0
  98. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/WHEEL +1 -1
  99. absfuyu/extra/data_analysis.py +0 -1078
  100. absfuyu/general/generator.py +0 -303
  101. absfuyu-5.0.0.dist-info/RECORD +0 -68
  102. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/entry_points.txt +0 -0
  103. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/licenses/LICENSE +0 -0
absfuyu/tools/sw.py ADDED
@@ -0,0 +1,718 @@
1
+ """
2
+ Absufyu: Software
3
+ -----------------
4
+ Software, pyinstaller related stuff
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = [
13
+ # Function
14
+ "get_system_info",
15
+ "get_pyinstaller_exe_dir",
16
+ "get_pyinstaller_resource_path",
17
+ "PyinstallerHelper",
18
+ "HWIDgen",
19
+ "LicenseKeySystem",
20
+ "BasicSoftwareProtection",
21
+ # Support
22
+ "SystemInfo",
23
+ "LicenseKey",
24
+ "PyinstallerHiddenImportPreset",
25
+ ]
26
+
27
+
28
+ # Library
29
+ # ---------------------------------------------------------------------------
30
+ import base64
31
+ import hashlib
32
+ import hmac
33
+ import json
34
+ import os
35
+ import platform
36
+ import socket
37
+ import subprocess
38
+ import sys
39
+ import uuid
40
+ from datetime import datetime
41
+ from pathlib import Path
42
+ from typing import Literal, NamedTuple, TypedDict
43
+
44
+ from absfuyu.core.baseclass import BaseClass
45
+ from absfuyu.core.docstring import versionadded, versionchanged
46
+ from absfuyu.dxt import Text
47
+ from absfuyu.tools.converter import Base64EncodeDecode
48
+ from absfuyu.util import stop_after_day
49
+
50
+
51
+ # MARK: System Info
52
+ # ---------------------------------------------------------------------------
53
+ class SystemInfo(NamedTuple):
54
+ """System info"""
55
+
56
+ os_type: str | Literal["Windows", "Linux", "Darwin"]
57
+ os_kernel: str
58
+ os_arch: str
59
+ system_name: str
60
+
61
+
62
+ def get_system_info() -> SystemInfo:
63
+ """
64
+ Returns the current operating system info.
65
+
66
+ The function attempts to retrieve the computer name using `platform.node()`.
67
+ If that fails or returns an empty string, it falls back to environment variables
68
+ or `socket.gethostname()` as a last resort, depending on the OS.
69
+
70
+ Returns
71
+ -------
72
+ SystemInfo
73
+ A tuple containing:
74
+ - OS name (e.g., 'Windows', 'Linux', 'Darwin')
75
+ - OS kernel (version)
76
+ - OS arch
77
+ - Computer name (hostname)
78
+ """
79
+ os_name = platform.system()
80
+
81
+ # Get computer name
82
+ try:
83
+ computer_name = platform.node()
84
+ if not computer_name:
85
+ raise ValueError("Empty name")
86
+ except ValueError:
87
+ if os_name == "Windows":
88
+ computer_name = os.environ.get("COMPUTERNAME", "Unknown")
89
+ else:
90
+ computer_name = os.environ.get("HOSTNAME", socket.gethostname())
91
+
92
+ # Return
93
+ return SystemInfo(
94
+ os_name, platform.version(), platform.machine().lower(), computer_name
95
+ )
96
+
97
+
98
+ # MARK: Pyinstaller
99
+ # ---------------------------------------------------------------------------
100
+ @versionchanged("5.6.1", "Fixed behavior")
101
+ def get_pyinstaller_exe_dir() -> Path:
102
+ """
103
+ Returns the directory where the current script or executable resides.
104
+
105
+ This function is useful for locating resources relative to the running script or
106
+ bundled executable (e.g., when using PyInstaller). It checks if the script is
107
+ running in a "frozen" state (as an executable), and returns the appropriate
108
+ directory accordingly.
109
+
110
+ Returns
111
+ -------
112
+ Path
113
+ A `pathlib.Path` object representing the directory containing the
114
+ executable or the current script file.
115
+ """
116
+ if getattr(sys, "frozen", False):
117
+ return Path(sys.executable).parent
118
+ else:
119
+ return Path(sys.argv[0]).resolve().parent
120
+
121
+
122
+ def get_pyinstaller_resource_path(relative_path: str) -> Path:
123
+ r"""
124
+ Get the absolute path to a resource file, compatible with both development
125
+ environments and PyInstaller-packaged executables.
126
+
127
+ When running from a PyInstaller bundle, this function resolves the path relative
128
+ to the temporary ``_MEIPASS`` folder. During normal execution, it resolves the path
129
+ relative to the current script's directory.
130
+
131
+ Parameters
132
+ ----------
133
+ relative_path : str
134
+ Relative path to the resource file or directory.
135
+
136
+ Returns
137
+ -------
138
+ Path
139
+ A `pathlib.Path` object pointing to the absolute location of the resource.
140
+
141
+
142
+ Example:
143
+ --------
144
+ >>> get_pyinstaller_resource_path("assets/logo.png")
145
+ <path>\assets\logo.png
146
+ """
147
+ if hasattr(sys, "_MEIPASS"):
148
+ # PyInstaller temp folder
149
+ base_path = Path(getattr(sys, "_MEIPASS")) # type: ignore[attr-defined]
150
+ else:
151
+ base_path = Path(__file__).resolve().parent
152
+
153
+ return base_path / relative_path
154
+
155
+
156
+ class PyinstallerHiddenImportPreset:
157
+ """
158
+ pyinstaller hidden import preset (library preset)
159
+
160
+ Example:
161
+ --------
162
+ >>> PyinstallerHelper(...).add_hidden_import(*PyinstallerHiddenImportPreset.ABSFUYU)
163
+ """
164
+
165
+ ABSFUYU = ["absfuyu"]
166
+ DF = ["pandas", "numpy", "openpyxl", "xlsxwriter"] # DataFrame
167
+ VISUAL = ["rich", "tqdm"]
168
+
169
+
170
+ @versionadded("5.11.0")
171
+ class PyinstallerHelper(BaseClass):
172
+ """pyinstaller helper"""
173
+
174
+ def __init__(
175
+ self,
176
+ path_to_file: str | Path,
177
+ relative_to_cwd: bool = True,
178
+ console: bool = True,
179
+ onefile: bool = False,
180
+ noconfirm: bool = False,
181
+ ) -> None:
182
+ """
183
+ pyinstaller cmd helper
184
+
185
+ Parameters
186
+ ----------
187
+ path_to_file : str | Path
188
+ Path to .py file to make .exe
189
+
190
+ relative_to_cwd : bool, optional
191
+ Is the file relative to cwd, by default True
192
+
193
+ console : bool, optional
194
+ Include console, by default True
195
+
196
+ onefile : bool, optional
197
+ Convert into one file, by default False
198
+
199
+ noconfirm : bool, optional
200
+ No confirmation, by default False
201
+ """
202
+ self.source_path = Path(path_to_file)
203
+ self.relative_to_cwd = relative_to_cwd
204
+ self.console = console
205
+ self.onefile = onefile
206
+ self.noconfirm = noconfirm
207
+
208
+ if self.relative_to_cwd:
209
+ # rel = self.source_path.relative_to(Path.cwd())
210
+ # self._base_cmd = ["pyinstaller", f"'.\\{Path('.').joinpath(rel)}'"]
211
+ self._base_cmd = ["pyinstaller", f"'.\\{self.source_path.relative_to(Path.cwd())}'"]
212
+ else:
213
+ self._base_cmd = ["pyinstaller", f"'{self.source_path.resolve()}'"]
214
+
215
+ self._hidden_import = []
216
+ self._icon = ""
217
+
218
+ def add_hidden_import(self, *library: str) -> None:
219
+ """
220
+ Add hidden import (library)
221
+ """
222
+ if len(self._hidden_import) < 1:
223
+ self._hidden_import = list(library)
224
+ else:
225
+ self._hidden_import.extend(list(library))
226
+
227
+ def add_icon(self, path_to_icon: str | Path, *, relative_to_cwd: bool | None = None) -> None:
228
+ """
229
+ Add icon to .exe
230
+
231
+ Parameters
232
+ ----------
233
+ path_to_icon : str | Path
234
+ Path to icon file
235
+
236
+ relative_to_cwd : bool | None, optional
237
+ Use boolean value to overwrite relative_to_cwd option of the main engine, by default None
238
+ """
239
+ p = Path(path_to_icon)
240
+ use_relative = self.relative_to_cwd if relative_to_cwd is None else relative_to_cwd
241
+
242
+ # --icon=favicon.ico
243
+ if use_relative:
244
+ rel = p.relative_to(Path.cwd())
245
+ # ensure it always shows with ./ prefix
246
+ # self._icon = f"'{Path('.').joinpath(rel)}'"
247
+ self._icon = f"--icon='.\\{rel}'"
248
+ else:
249
+ self._icon = f"--icon='{p.resolve()}'"
250
+
251
+ def export_cmd(self) -> str:
252
+ """
253
+ Export pyinstaller cmd
254
+
255
+ Returns
256
+ -------
257
+ str
258
+ pyinstaller command
259
+ """
260
+ cmd = self._base_cmd
261
+
262
+ # Hidden import
263
+ if len(self._hidden_import) > 0:
264
+ dat = (f"--hidden-import={x}" for x in list(set(self._hidden_import)))
265
+ cmd.append(" ".join(dat))
266
+
267
+ # Console
268
+ if not self.console:
269
+ cmd.append("--noconsole")
270
+
271
+ # One file
272
+ if self.onefile:
273
+ cmd.append("--onefile")
274
+
275
+ # No confirm
276
+ if self.noconfirm:
277
+ cmd.append("--noconfirm")
278
+
279
+ # Icon
280
+ if len(self._icon) > 0:
281
+ cmd.append(self._icon)
282
+
283
+ return " ".join(cmd)
284
+
285
+
286
+ # MARK: Key System
287
+ # ---------------------------------------------------------------------------
288
+ class HWIDgen(BaseClass):
289
+ """
290
+ Generate Hardware ID (HWID)
291
+
292
+
293
+ Example:
294
+ --------
295
+ >>> HWIDgen.generate()
296
+ """
297
+
298
+ def __init__(self) -> None:
299
+ pass
300
+
301
+ @classmethod
302
+ def generate(cls) -> str:
303
+ """Generate HWID for current system"""
304
+ os_type = platform.system().lower()
305
+ if os_type == "windows":
306
+ return cls._get_windows_hwid()
307
+ elif os_type == "linux":
308
+ return cls._get_linux_hwid()
309
+ else:
310
+ return cls._get_hwid_mac()
311
+
312
+ @staticmethod
313
+ def _get_hwid_mac() -> str:
314
+ """HWID: MAC address"""
315
+ mac = uuid.getnode() # 48-bit MAC address
316
+ mac_str = ":".join(("%012X" % mac)[i : i + 2] for i in range(0, 12, 2))
317
+ hwid = hashlib.sha256(mac_str.encode()).hexdigest()
318
+ return hwid
319
+
320
+ @staticmethod
321
+ def _get_windows_hwid() -> str:
322
+ try:
323
+ # Get BIOS serial number
324
+ bios = (
325
+ subprocess.check_output("wmic bios get serialnumber", shell=True)
326
+ .decode()
327
+ .split("\n")[1]
328
+ .strip()
329
+ )
330
+
331
+ # Get Motherboard serial
332
+ board = (
333
+ subprocess.check_output("wmic baseboard get serialnumber", shell=True)
334
+ .decode()
335
+ .split("\n")[1]
336
+ .strip()
337
+ )
338
+
339
+ # Get Disk serial
340
+ disk = (
341
+ subprocess.check_output("wmic diskdrive get serialnumber", shell=True)
342
+ .decode()
343
+ .split("\n")[1]
344
+ .strip()
345
+ )
346
+
347
+ raw = bios + board + disk
348
+ hwid = hashlib.sha256(raw.encode()).hexdigest()
349
+ return hwid
350
+ except Exception as e:
351
+ return f"Error getting HWID: {e}"
352
+
353
+ @staticmethod
354
+ def _get_linux_hwid() -> str:
355
+ try:
356
+ disk_info = subprocess.check_output(
357
+ "udevadm info --query=all --name=/dev/sda", shell=True
358
+ ).decode()
359
+ serial = ""
360
+ for line in disk_info.splitlines():
361
+ if "ID_SERIAL_SHORT" in line:
362
+ serial = line.split("=")[1]
363
+ break
364
+
365
+ mac = uuid.getnode()
366
+ mac_str = ":".join(("%012X" % mac)[i : i + 2] for i in range(0, 12, 2))
367
+
368
+ raw = serial + mac_str
369
+ hwid = hashlib.sha256(raw.encode()).hexdigest()
370
+ return hwid
371
+ except Exception as e:
372
+ return f"Error getting HWID: {e}"
373
+
374
+
375
+ class LicenseKey(TypedDict):
376
+ name: str
377
+ expiry: str
378
+ signature: str
379
+
380
+
381
+ class LicenseKeySystem(BaseClass):
382
+ def __init__(self, name: str, expiry: str, secret_key: str) -> None:
383
+ """
384
+ License Key implementation
385
+
386
+ Parameters
387
+ ----------
388
+ name : str
389
+ Name of license holder
390
+
391
+ expiry : str
392
+ Expiry date (in yyyy-mm-dd format)
393
+
394
+ secret_key : str
395
+ Secret key to make license key
396
+ """
397
+ self._name = name
398
+ self._expiry = expiry
399
+ self._secret = secret_key.encode("utf-8")
400
+
401
+ # Make key
402
+ def make_key(self, *, hwid_overwrite: str | None = None) -> str:
403
+ """
404
+ Make a license key in these following steps:
405
+ 1. Generate HWID
406
+ 2. Combine name, expiry, HWID -> sign it
407
+ 3. Base64-encode the signature
408
+ 4. Store name, expiry, signature in JSON -> Base64 -> Hex
409
+ 5. Divide by 30 chars per line
410
+ 6. Wrap in BEGIN/END KEY
411
+
412
+ Parameters
413
+ ----------
414
+ hwid_overwrite : str | None, optional
415
+ Overwrite the HWID, by default None
416
+
417
+ Returns
418
+ -------
419
+ str
420
+ License key
421
+ """
422
+ # Prepare
423
+ hwid = (
424
+ HWIDgen.generate() if hwid_overwrite is None else hwid_overwrite
425
+ ) # Get HWID
426
+ msg_data = f"{self._name}|{self._expiry}|{hwid}" # Make msg
427
+ signature = hmac.new(self._secret, msg_data.encode(), hashlib.sha256).digest()
428
+ encoded_sig = base64.urlsafe_b64encode(signature).decode()
429
+
430
+ # Key format
431
+ key: LicenseKey = {
432
+ "name": self._name,
433
+ "expiry": self._expiry,
434
+ "signature": encoded_sig,
435
+ }
436
+ # This convert to .json -> Base64 -> Hex
437
+ encoded_key = Text(Base64EncodeDecode.encode(json.dumps(key))).to_hex(raw=True)
438
+ output_key = (
439
+ "BEGIN KEY".center(30, "=")
440
+ + "\n"
441
+ + "\n".join(Text(encoded_key).divide(30))
442
+ + "\n"
443
+ + "END KEY".center(30, "=")
444
+ )
445
+ return output_key
446
+
447
+ # Check key
448
+ @staticmethod
449
+ def _parse_license_key(license_key: str) -> LicenseKey:
450
+ """
451
+ Parse a formatted license key
452
+
453
+ Parameters
454
+ ----------
455
+ license_key : str
456
+ License key
457
+
458
+ Returns
459
+ -------
460
+ LicenseKey
461
+ Parsed license key
462
+ """
463
+ try:
464
+ lines = license_key.strip().split("\n")[1:-1] # Remove BEGIN/END KEY lines
465
+ raw_hex = "".join(lines)
466
+ decoded_json = Base64EncodeDecode.decode(
467
+ bytes.fromhex(raw_hex).decode("utf-8")
468
+ )
469
+ return json.loads(decoded_json)
470
+ except Exception as e:
471
+ raise ValueError("Invalid license key format") from e
472
+
473
+ @classmethod
474
+ def verify_license(
475
+ cls,
476
+ license_key: str,
477
+ secret_key: str | None = None,
478
+ hwid_overwrite: str | None = None,
479
+ ) -> bool:
480
+ """
481
+ Verify a license key
482
+
483
+ Parameters
484
+ ----------
485
+ license_key : str
486
+ License key
487
+
488
+ secret_key : str | None, optional
489
+ Secret key, by default None
490
+
491
+ hwid_overwrite : str | None, optional
492
+ HWID, by default None
493
+
494
+ Returns
495
+ -------
496
+ bool
497
+ _description_
498
+ """
499
+
500
+ # Prep
501
+ secret = "" if secret_key is None else secret_key
502
+ hwid = HWIDgen.generate() if hwid_overwrite is None else hwid_overwrite
503
+ parsed_license_key = cls._parse_license_key(license_key)
504
+
505
+ try:
506
+ msg_data = (
507
+ f"{parsed_license_key['name']}|{parsed_license_key['expiry']}|{hwid}"
508
+ )
509
+
510
+ expected_sig = hmac.new(
511
+ secret.encode("utf-8"), msg_data.encode(), hashlib.sha256
512
+ ).digest()
513
+ expected_encoded_sig = base64.urlsafe_b64encode(expected_sig).decode()
514
+
515
+ return parsed_license_key["signature"] == expected_encoded_sig
516
+ except Exception:
517
+ return False
518
+
519
+
520
+ # MARK: Software
521
+ # ---------------------------------------------------------------------------
522
+ class BasicSoftwareProtection(BaseClass):
523
+ """
524
+ Basic software protection
525
+
526
+ This check valid license before run any app. Recommended to put at start of the code
527
+
528
+ Usage:
529
+ ------
530
+ >>> t = BasicSoftwareProtection(get_pyinstaller_exe_dir())
531
+ >>> t.add_secret("Test Key")
532
+ >>> t.check_valid_license()
533
+ """
534
+
535
+ def __init__(
536
+ self,
537
+ cwd: str | Path,
538
+ name: str | None = None,
539
+ version: str | None = None,
540
+ author: str | None = None,
541
+ author_email: str | None = None,
542
+ ) -> None:
543
+ """
544
+ Basic software protection.
545
+
546
+ Parameters
547
+ ----------
548
+ cwd : str | Path
549
+ Current working directory
550
+
551
+ name : str | None, optional
552
+ Name of the software, by default None
553
+
554
+ version : str | None, optional
555
+ Version of the software, by default None
556
+
557
+ author : str | None, optional
558
+ Author of the software, by default None
559
+
560
+ author_email : str | None, optional
561
+ Author's email of the software, by default None
562
+ """
563
+ self._cwd = Path(cwd)
564
+ self._author = "" if author is None else author
565
+ self._author_email = "" if author_email is None else author_email
566
+ self._software_name = "" if name is None else name
567
+ self._software_version = "" if version is None else version
568
+ self._secret = ""
569
+
570
+ # Metadata
571
+ @property
572
+ def cwd(self) -> Path:
573
+ """Current working directory"""
574
+ return self._cwd
575
+
576
+ @property
577
+ def software_name(self) -> str:
578
+ """Name of the software"""
579
+ return self._software_name
580
+
581
+ @software_name.setter
582
+ def software_name(self, value: str) -> None:
583
+ # Logic to validate name
584
+ self._software_name = value
585
+
586
+ @property
587
+ def author(self) -> str:
588
+ """Author of the software"""
589
+ return self._author
590
+
591
+ @property
592
+ def version(self) -> str:
593
+ """Version of the software"""
594
+ return self._software_version
595
+
596
+ # Protection
597
+ def add_secret(self, secret: str) -> None:
598
+ """
599
+ Add secret
600
+
601
+ Parameters
602
+ ----------
603
+ secret : str
604
+ secret
605
+ """
606
+ self._secret = secret
607
+
608
+ def check_valid_license(self, generate_helper: bool = True) -> None:
609
+ """
610
+ Check for valid license
611
+ (``.zlic`` file in the same directory as the script that runs this code)
612
+
613
+ Parameters
614
+ ----------
615
+ generate_helper : bool, optional
616
+ Generate a helper file (``license.helper``),
617
+ which can be used to make license key,
618
+ by default ``True``
619
+
620
+ Raises
621
+ ------
622
+ SystemExit
623
+ License file not found!
624
+
625
+ SystemExit
626
+ Invalid license key format!
627
+
628
+ SystemExit
629
+ Invalid license!
630
+ """
631
+ try:
632
+ # Get license file
633
+ license_file = list(self._cwd.glob("*.zlic"))[0]
634
+ with license_file.open() as f:
635
+ # Load data
636
+ data = "".join(f.readlines())
637
+ except IndexError:
638
+ if generate_helper:
639
+ self.generate_license_helper()
640
+ raise SystemExit("License file not found!")
641
+ except ValueError:
642
+ raise SystemExit("Invalid license key format!")
643
+ else:
644
+ # Verify license
645
+ if LicenseKeySystem.verify_license(data, secret_key=self._secret):
646
+ parsed_date = datetime.strptime(
647
+ LicenseKeySystem._parse_license_key(data)["expiry"], "%Y-%m-%d"
648
+ )
649
+ stop_after_day(
650
+ parsed_date.year,
651
+ parsed_date.month,
652
+ parsed_date.day,
653
+ custom_msg="License expired!",
654
+ )
655
+ else: # Invalid license
656
+ raise SystemExit("Invalid license!")
657
+
658
+ @versionadded("5.9.0")
659
+ def check_valid_license_gui(
660
+ self, title: str = "WARNING", message: str = "INVALID LICENSE!", generate_helper: bool = True
661
+ ) -> None:
662
+ """
663
+ Check for valid license
664
+ (``.zlic`` file in the same directory as the script that runs this code)
665
+ but will pop up a GUI when invalid license.
666
+
667
+ Parameters
668
+ ----------
669
+ title : str, optional
670
+ Title of the GUI, by default "WARNING"
671
+
672
+ message : str, optional
673
+ Message in the GUI, by default "INVALID LICENSE!"
674
+
675
+ generate_helper : bool, optional
676
+ Generate a helper file (``license.helper``),
677
+ which can be used to make license key,
678
+ by default ``True``
679
+
680
+ Raises
681
+ ------
682
+ SystemExit
683
+ Invalid license
684
+ """
685
+ try:
686
+ self.check_valid_license(generate_helper=generate_helper)
687
+ except SystemExit:
688
+ from tkinter import messagebox
689
+
690
+ messagebox.showwarning(title, message, icon="error")
691
+ raise SystemExit("Invalid license")
692
+
693
+ # Make key
694
+ def _make_key(self, name: str, expiry: str, secret: str) -> None:
695
+ """
696
+ Generate license key in the same directory as the script that runs this code.
697
+
698
+ Parameters
699
+ ----------
700
+ name : str
701
+ Name of license's holder
702
+
703
+ expiry : str
704
+ Expiry date in format "yyyy-mm-dd"
705
+
706
+ secret : str
707
+ Secret string
708
+ """
709
+ path = self._cwd.joinpath("license.zlic")
710
+ engine = LicenseKeySystem(name, expiry, secret)
711
+ with path.open("w", encoding="utf-8") as f:
712
+ f.write(engine.make_key())
713
+
714
+ def generate_license_helper(self) -> None:
715
+ """Gather HWID and make it into a file"""
716
+ path = self._cwd.joinpath("license.helper")
717
+ with path.open("w", encoding="utf-8") as f:
718
+ f.write(HWIDgen.generate())