absfuyu 5.6.1__py3-none-any.whl → 6.1.3__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 (102) hide show
  1. absfuyu/__init__.py +5 -3
  2. absfuyu/__main__.py +2 -2
  3. absfuyu/cli/__init__.py +13 -2
  4. absfuyu/cli/audio_group.py +98 -0
  5. absfuyu/cli/color.py +2 -2
  6. absfuyu/cli/config_group.py +2 -2
  7. absfuyu/cli/do_group.py +2 -2
  8. absfuyu/cli/game_group.py +20 -2
  9. absfuyu/cli/tool_group.py +68 -4
  10. absfuyu/config/__init__.py +3 -3
  11. absfuyu/core/__init__.py +10 -6
  12. absfuyu/core/baseclass.py +104 -34
  13. absfuyu/core/baseclass2.py +43 -2
  14. absfuyu/core/decorator.py +2 -2
  15. absfuyu/core/docstring.py +4 -2
  16. absfuyu/core/dummy_cli.py +3 -3
  17. absfuyu/core/dummy_func.py +2 -2
  18. absfuyu/dxt/__init__.py +2 -2
  19. absfuyu/dxt/base_type.py +93 -0
  20. absfuyu/dxt/dictext.py +188 -6
  21. absfuyu/dxt/dxt_support.py +2 -2
  22. absfuyu/dxt/intext.py +72 -4
  23. absfuyu/dxt/listext.py +495 -23
  24. absfuyu/dxt/strext.py +2 -2
  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 +2 -2
  31. absfuyu/extra/da/__init__.py +39 -3
  32. absfuyu/extra/da/dadf.py +458 -29
  33. absfuyu/extra/da/dadf_base.py +2 -2
  34. absfuyu/extra/da/df_func.py +89 -5
  35. absfuyu/extra/da/mplt.py +2 -2
  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 +4 -6
  45. absfuyu/extra/rclone.py +253 -0
  46. absfuyu/extra/xml.py +90 -0
  47. absfuyu/fun/__init__.py +2 -20
  48. absfuyu/fun/rubik.py +2 -2
  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 -2
  55. absfuyu/game/wordle.py +6 -4
  56. absfuyu/general/__init__.py +2 -2
  57. absfuyu/general/content.py +2 -2
  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 +2 -2
  72. absfuyu/tools/checksum.py +119 -4
  73. absfuyu/tools/converter.py +2 -2
  74. absfuyu/tools/generator.py +24 -7
  75. absfuyu/tools/inspector.py +2 -2
  76. absfuyu/tools/keygen.py +2 -2
  77. absfuyu/tools/obfuscator.py +2 -2
  78. absfuyu/tools/passwordlib.py +2 -2
  79. absfuyu/tools/shutdownizer.py +3 -8
  80. absfuyu/tools/sw.py +213 -10
  81. absfuyu/tools/web.py +10 -13
  82. absfuyu/typings.py +5 -8
  83. absfuyu/util/__init__.py +31 -2
  84. absfuyu/util/api.py +7 -4
  85. absfuyu/util/cli.py +119 -0
  86. absfuyu/util/gui.py +91 -0
  87. absfuyu/util/json_method.py +2 -2
  88. absfuyu/util/lunar.py +2 -2
  89. absfuyu/util/package.py +124 -0
  90. absfuyu/util/path.py +313 -4
  91. absfuyu/util/performance.py +2 -2
  92. absfuyu/util/shorten_number.py +206 -13
  93. absfuyu/util/text_table.py +2 -2
  94. absfuyu/util/zipped.py +2 -2
  95. absfuyu/version.py +22 -19
  96. {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/METADATA +37 -8
  97. absfuyu-6.1.3.dist-info/RECORD +105 -0
  98. {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/WHEEL +1 -1
  99. absfuyu/extra/data_analysis.py +0 -21
  100. absfuyu-5.6.1.dist-info/RECORD +0 -79
  101. {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/entry_points.txt +0 -0
  102. {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -3,22 +3,33 @@ Absfuyu: Data Analysis
3
3
  ----------------------
4
4
  DF Function
5
5
 
6
- Version: 5.6.1
7
- Date updated: 12/09/2025 (dd/mm/yyyy)
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module level
11
11
  # ---------------------------------------------------------------------------
12
- __all__ = ["equalize_df", "compare_2_list", "rename_with_dict"]
12
+ __all__ = [
13
+ "equalize_df",
14
+ "compare_2_list",
15
+ "rename_with_dict",
16
+ "merge_data_files",
17
+ ]
13
18
 
14
19
 
15
20
  # Library
16
21
  # ---------------------------------------------------------------------------
22
+ from collections.abc import Mapping
17
23
  from itertools import chain
24
+ from pathlib import Path
25
+ from typing import Literal, cast
18
26
 
19
27
  import numpy as np
20
28
  import pandas as pd
21
29
 
30
+ from absfuyu.core.docstring import versionadded
31
+ from absfuyu.core.dummy_func import tqdm
32
+
22
33
 
23
34
  # Function
24
35
  # ---------------------------------------------------------------------------
@@ -73,8 +84,7 @@ def compare_2_list(*arr) -> pd.DataFrame:
73
84
 
74
85
  df = pd.DataFrame(temp_dict)
75
86
  df["Compare"] = np.where(
76
- df[f"{col_name}0"].apply(lambda x: str(x).lower())
77
- == df[f"{col_name}1"].apply(lambda x: str(x).lower()),
87
+ df[f"{col_name}0"].apply(lambda x: str(x).lower()) == df[f"{col_name}1"].apply(lambda x: str(x).lower()),
78
88
  df[f"{col_name}0"], # Value when True
79
89
  np.nan, # Value when False
80
90
  )
@@ -95,3 +105,77 @@ def rename_with_dict(df: pd.DataFrame, col: str, rename_dict: dict) -> pd.DataFr
95
105
  rename_val = list(rename_dict.keys())
96
106
  df[name] = df[name].apply(lambda x: "Other" if x in rename_val else x)
97
107
  return df
108
+
109
+
110
+ @versionadded("6.0.0")
111
+ def merge_data_files(
112
+ work_dir: Path | str,
113
+ file_type: Literal[".csv", ".xls", ".xlsx"] = ".xlsx",
114
+ output_file: Path | str | None = None,
115
+ *,
116
+ tqdm_enabled: bool = True,
117
+ ) -> None:
118
+ """
119
+ Merge all data-sheet-like (.csv, .xls, .xlsx) in a folder/directory.
120
+ Also remove duplicate rows
121
+
122
+ Parameters
123
+ ----------
124
+ work_dir : Path | str
125
+ Files in which folder/directory
126
+
127
+ file_type : Literal[".csv", ".xls", ".xlsx"], optional
128
+ File format, by default ``".xlsx"``
129
+
130
+ output_file : Path | str | None, optional
131
+ | Output file location, by default ``None``
132
+ | File will be export in ``.xlsx`` format
133
+ | Default export name is ``data_merged.xlsx``
134
+
135
+ tqdm_enabled : bool, optional
136
+ Use ``tqdm`` package to show progress bar (if available), by default ``True``
137
+ """
138
+
139
+ default_name = "data_merged.xlsx"
140
+ paths = [x for x in Path(work_dir).glob(f"**/*{file_type}") if x.name != default_name]
141
+ output_path = Path(output_file) if output_file is not None else Path(work_dir).joinpath(default_name)
142
+
143
+ dfs = []
144
+ if tqdm_enabled:
145
+ for x in tqdm(paths, desc="Merging files", unit_scale=True):
146
+ dfs.append(pd.read_excel(x))
147
+ else:
148
+ for x in paths:
149
+ dfs.append(pd.read_excel(x))
150
+
151
+ df = cast(pd.DataFrame, pd.concat(dfs, axis=0, join="inner")).drop_duplicates().reset_index()
152
+ df.drop(columns=["index"], inplace=True)
153
+
154
+ df.to_excel(output_path, index=False)
155
+
156
+
157
+ @versionadded("6.0.0")
158
+ def export_dfs_to_excel(
159
+ path: str,
160
+ dfs: Mapping[str, pd.DataFrame],
161
+ *,
162
+ index: bool = False,
163
+ ) -> None:
164
+ """
165
+ Export multiple DataFrames into one Excel file.
166
+
167
+ Parameters
168
+ ----------
169
+ path : str
170
+ Output Excel file path.
171
+
172
+ dfs : Mapping[str, DataFrame]
173
+ Sheet name -> DataFrame mapping.
174
+
175
+ index : bool, default False
176
+ Whether to include DataFrame index, by default ``False``
177
+ """
178
+ with pd.ExcelWriter(path, engine="openpyxl") as writer:
179
+ for name, df in dfs.items():
180
+ name = name[:31] # Excel sheet name length limit
181
+ df.to_excel(writer, sheet_name=name, index=index)
absfuyu/extra/da/mplt.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Data Analysis
3
3
  ----------------------
4
4
  Matplotlib Helper
5
5
 
6
- Version: 5.6.1
7
- Date updated: 12/09/2025 (dd/mm/yyyy)
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module level
@@ -0,0 +1,8 @@
1
+ """
2
+ Absfuyu: Google related
3
+ -----------------------
4
+ Google API
5
+
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
@@ -0,0 +1,223 @@
1
+ """
2
+ Absfuyu: Google related
3
+ -----------------------
4
+ Google Drive downloader
5
+
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["GoogleDriveFile", "GoogleDriveClient"]
13
+
14
+
15
+ # Library
16
+ # ---------------------------------------------------------------------------
17
+ import io
18
+ import mimetypes
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import ClassVar, cast
22
+
23
+ from absfuyu.core.baseclass import BaseClass
24
+
25
+ try:
26
+ import requests
27
+ from google.oauth2 import service_account
28
+ from googleapiclient.discovery import Resource, build
29
+ from googleapiclient.http import MediaIoBaseDownload
30
+ except ImportError:
31
+ from subprocess import run
32
+
33
+ from absfuyu.config import ABSFUYU_CONFIG
34
+
35
+ if ABSFUYU_CONFIG._get_setting("auto-install-extra").value: # type: ignore
36
+ cmd = "python -m pip install -U absfuyu[ggapi]".split()
37
+ run(cmd)
38
+ else:
39
+ raise SystemExit("This feature is in absfuyu[ggapi] package") # noqa: B904
40
+
41
+
42
+ # Class
43
+ # ---------------------------------------------------------------------------
44
+ @dataclass(slots=True)
45
+ class DriveFileMeta:
46
+ id: str
47
+ name: str
48
+ mime_type: str
49
+
50
+
51
+ class GoogleDriveClient(BaseClass):
52
+ SCOPES: ClassVar = [
53
+ "https://www.googleapis.com/auth/drive",
54
+ "https://www.googleapis.com/auth/spreadsheets",
55
+ ]
56
+
57
+ EXPORT_MIME_MAP: ClassVar = {
58
+ "application/vnd.google-apps.spreadsheet": (
59
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
60
+ ".xlsx",
61
+ ),
62
+ "application/vnd.google-apps.document": (
63
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
64
+ ".docx",
65
+ ),
66
+ "application/vnd.google-apps.presentation": (
67
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
68
+ ".pptx",
69
+ ),
70
+ }
71
+
72
+ def __init__(self, service_account_data: str | Path | dict[str, str]) -> None:
73
+ """
74
+ Google drive instance
75
+
76
+ Parameters
77
+ ----------
78
+ service_account_data : str | Path | dict[str, str]
79
+ Path to ``service_account.json`` file or .json loaded data
80
+ """
81
+ if isinstance(service_account_data, str):
82
+ self.credentials = service_account.Credentials.from_service_account_file(
83
+ service_account_data,
84
+ scopes=self.SCOPES,
85
+ )
86
+ elif isinstance(service_account_data, Path):
87
+ self.credentials = service_account.Credentials.from_service_account_file(
88
+ str(service_account_data.resolve()),
89
+ scopes=self.SCOPES,
90
+ )
91
+ else:
92
+ self.credentials = service_account.Credentials.from_service_account_info(
93
+ service_account_data,
94
+ scopes=self.SCOPES,
95
+ )
96
+
97
+ self.drive = cast(Resource, build("drive", "v3", credentials=self.credentials))
98
+ self.sheets = cast(Resource, build("sheets", "v4", credentials=self.credentials))
99
+
100
+ # Metadata
101
+ # -----------------------------
102
+ def get_metadata(self, file_id: str) -> DriveFileMeta:
103
+ data = self.drive.files().get(fileId=file_id, fields="id,name,mimeType").execute()
104
+ return DriveFileMeta(
105
+ id=data["id"],
106
+ name=data["name"],
107
+ mime_type=data["mimeType"],
108
+ )
109
+
110
+ # Download / Export
111
+ # -----------------------------
112
+ def download_bytes(self, file_id: str) -> bytes:
113
+ request = self.drive.files().get_media(fileId=file_id)
114
+ return self._download(request)
115
+
116
+ def export_bytes(self, file_id: str, mime_type: str) -> bytes:
117
+ request = self.drive.files().export_media(
118
+ fileId=file_id,
119
+ mimeType=mime_type,
120
+ )
121
+ return self._download(request)
122
+
123
+ def _download(self, request) -> bytes:
124
+ buf = io.BytesIO()
125
+ downloader = MediaIoBaseDownload(buf, request)
126
+ done = False
127
+ while not done:
128
+ _, done = downloader.next_chunk()
129
+ buf.seek(0)
130
+ return buf.read()
131
+
132
+
133
+ class GoogleDriveFile:
134
+ """
135
+ Google drive file to download
136
+
137
+
138
+ Example:
139
+ --------
140
+ >>> client = GoogleDriveClient("service_account.json")
141
+ >>> gfile = GoogleDriveFile(file_id=<id>, client=client)
142
+ >>> gfile.download(<dir>)
143
+ """
144
+
145
+ PUBLIC_URL = "https://drive.google.com/uc?export=download"
146
+
147
+ def __init__(
148
+ self,
149
+ file_id: str,
150
+ *,
151
+ client: GoogleDriveClient | None = None,
152
+ timeout: float = 30.0,
153
+ ) -> None:
154
+ self.file_id = file_id
155
+ self.client = client
156
+ self.timeout = timeout
157
+ self.session = requests.Session()
158
+
159
+ # Public API
160
+ # -----------------------------
161
+ def download(self, directory: str | Path = ".") -> Path:
162
+ """
163
+ Download file with auto-detected name and extension.
164
+ Returns saved file path.
165
+ """
166
+ directory = Path(directory)
167
+ directory.mkdir(parents=True, exist_ok=True)
168
+
169
+ if self.client:
170
+ return self._download_private(directory)
171
+
172
+ return self._download_public(directory)
173
+
174
+ # Private (Service Account)
175
+ # -----------------------------
176
+ def _download_private(self, directory: Path) -> Path:
177
+ meta = self.client.get_metadata(self.file_id)
178
+
179
+ # Google Docs -> export
180
+ if meta.mime_type in self.client.EXPORT_MIME_MAP:
181
+ export_mime, ext = self.client.EXPORT_MIME_MAP[meta.mime_type]
182
+ data = self.client.export_bytes(self.file_id, export_mime)
183
+ filename = meta.name + ext
184
+ else:
185
+ data = self.client.download_bytes(self.file_id)
186
+ ext = mimetypes.guess_extension(meta.mime_type) or ""
187
+ filename = meta.name if Path(meta.name).suffix else meta.name + ext
188
+
189
+ path = directory / filename
190
+ path.write_bytes(data)
191
+ return path
192
+
193
+ # Public (no auth)
194
+ # -----------------------------
195
+ def _download_public(self, directory: Path) -> Path:
196
+ response = self.session.get(
197
+ self.PUBLIC_URL,
198
+ params={"id": self.file_id},
199
+ timeout=self.timeout,
200
+ )
201
+
202
+ # confirmation token
203
+ for k, v in response.cookies.items():
204
+ if k.startswith("download_warning"):
205
+ response = self.session.get(
206
+ self.PUBLIC_URL,
207
+ params={"id": self.file_id, "confirm": v},
208
+ timeout=self.timeout,
209
+ )
210
+ break
211
+
212
+ response.raise_for_status()
213
+
214
+ # filename from header (if present)
215
+ cd = response.headers.get("content-disposition", "")
216
+ filename = "downloaded_file"
217
+
218
+ if "filename=" in cd:
219
+ filename = cd.split("filename=")[-1].strip('"')
220
+
221
+ path = directory / filename
222
+ path.write_bytes(response.content)
223
+ return path
@@ -0,0 +1,148 @@
1
+ """
2
+ Absfuyu: Google related
3
+ -----------------------
4
+ Google Online license from sheet
5
+
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["GGSheetOnlineLicenseSystem"]
13
+
14
+
15
+ # Library
16
+ # ---------------------------------------------------------------------------
17
+ from dataclasses import dataclass
18
+ from datetime import datetime
19
+ from functools import cached_property
20
+ from pathlib import Path
21
+ from typing import Any, Self, cast
22
+
23
+ from absfuyu.core.baseclass import BaseClass
24
+ from absfuyu.extra.ggapi.gsheet import GoogleSheet
25
+ from absfuyu.tools.sw import HWIDgen, get_system_info
26
+
27
+ # Var
28
+ # ---------------------------------------------------------------------------
29
+ type GoogleSheetClient = GoogleSheet
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class LicenseEntry:
34
+ id: int
35
+ hwid: str
36
+ name: str
37
+ os: str
38
+ date: int | datetime
39
+ active: bool
40
+
41
+ def to_row(self) -> list:
42
+ return [self.id, self.hwid, self.name, self.os, self.date, self.active]
43
+
44
+
45
+ # Class
46
+ # ---------------------------------------------------------------------------
47
+ class GGSheetOnlineLicenseSystem(BaseClass):
48
+ def __init__(self, google_client: GoogleSheetClient, sheet_id: str, sheet_name: str = "Sheet1") -> None:
49
+ """
50
+ Google sheet online license system instance
51
+
52
+ Parameters
53
+ ----------
54
+ google_client : GoogleSheetClient
55
+ Google sheet client
56
+
57
+ sheet_id : str
58
+ Sheet ID
59
+
60
+ sheet_name : str, optional
61
+ Sheet name, by default ``"Sheet1"``
62
+ """
63
+ self.client = google_client
64
+ self._sheet_id = sheet_id
65
+ self._sheet_name = sheet_name
66
+
67
+ # Variable
68
+ self._df: list[LicenseEntry] | None = None
69
+ try:
70
+ self._load()
71
+ except Exception:
72
+ pass
73
+
74
+ # Property
75
+ @cached_property
76
+ def hwid(self) -> str:
77
+ return HWIDgen.generate()
78
+
79
+ # Support
80
+ def _load(self) -> None:
81
+ df = cast(list[list[Any]], self.client.read_sheet(spreadsheet_id=self._sheet_id, sheet_name=self._sheet_name))
82
+ data = [LicenseEntry(*x) for x in df]
83
+ self._df = data
84
+
85
+ def _gather_system_info(self) -> list[str]:
86
+ self._sysif = get_system_info()
87
+ return [self.hwid, self._sysif.system_name, self._sysif.os_type]
88
+
89
+ def _make_device_entry(self) -> list:
90
+ df = self._df
91
+ row = [int(df[-1].id) + 1]
92
+ row.extend(self._gather_system_info())
93
+ row.extend([datetime.now().strftime("%d/%m/%Y"), False])
94
+ return row
95
+
96
+ def _get_device_online(self):
97
+ """Get ID of device in sheet, create entry if None"""
98
+ df = self._df
99
+
100
+ for x in df:
101
+ if x.hwid == self.hwid:
102
+ return x.id
103
+
104
+ has_updated = False # safe guard
105
+ if not has_updated:
106
+ new_entry = self._make_device_entry()
107
+ self.client.append_rows(self._sheet_id, self._sheet_name, [new_entry])
108
+ has_updated = True
109
+ self._load()
110
+ return new_entry[0]
111
+
112
+ # Main
113
+ def license_check(self) -> None:
114
+ """Check if activated in sheet"""
115
+ df = self._df
116
+ id = self._get_device_online()
117
+
118
+ try:
119
+ for x in df:
120
+ if x.id == id:
121
+ active_status = x.active
122
+ if not active_status:
123
+ raise SystemExit("Not activated")
124
+ return None
125
+ except Exception:
126
+ raise SystemExit("Not activated")
127
+
128
+ # Classmethod
129
+ @classmethod
130
+ def from_service_account(
131
+ cls, service_account_data: str | Path | dict[str, str], sheet_id: str, sheet_name: str = "Sheet1"
132
+ ) -> Self:
133
+ """
134
+ Google sheet online license system instance
135
+
136
+ Parameters
137
+ ----------
138
+ service_account_data : str | Path | dict[str, str]
139
+ Path to ``service_account.json`` file or .json loaded data
140
+
141
+ sheet_id : str
142
+ Sheet ID
143
+
144
+ sheet_name : str, optional
145
+ Sheet name, by default ``"Sheet1"``
146
+ """
147
+ client = GoogleSheet(service_account_data)
148
+ return cls(client, sheet_id, sheet_name)
@@ -0,0 +1,186 @@
1
+ """
2
+ Absfuyu: Google related
3
+ -----------------------
4
+ Google Online license from sheet (DataFrame version)
5
+
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["GGSheetOnlineLicenseSystemDF"]
13
+
14
+
15
+ # Library
16
+ # ---------------------------------------------------------------------------
17
+ from datetime import datetime
18
+ from enum import StrEnum
19
+ from pathlib import Path
20
+ from typing import Self, override
21
+
22
+ import pandas as pd
23
+
24
+ from absfuyu.extra.ggapi.glicense import GGSheetOnlineLicenseSystem
25
+ from absfuyu.extra.ggapi.gsheet import GoogleSheet
26
+
27
+ # Var
28
+ # ---------------------------------------------------------------------------
29
+ AVAILABLE_COLUMNS = ["stt", "hwid", "pc_name", "os", "date", "active"]
30
+
31
+
32
+ class DF_COL(StrEnum):
33
+ TT = "stt"
34
+ HWID = "hwid"
35
+ NAME = "pc_name"
36
+ OS = "OS"
37
+ DATE = "date"
38
+ ACTIVE = "active"
39
+
40
+
41
+ # Class
42
+ # ---------------------------------------------------------------------------
43
+ class GoogleSheetDF(GoogleSheet):
44
+ # Sheets — READ (NO DOWNLOAD)
45
+ # ------------------------------------------------------------------
46
+ def read_sheet_as_dataframe(
47
+ self,
48
+ spreadsheet_id: str,
49
+ sheet_name: str = "Sheet1",
50
+ header: bool = True,
51
+ ) -> pd.DataFrame:
52
+ """
53
+ Read Google Sheet directly into DataFrame.
54
+ """
55
+ result = (
56
+ self.sheets.spreadsheets()
57
+ .values()
58
+ .get(
59
+ spreadsheetId=spreadsheet_id,
60
+ range=sheet_name,
61
+ valueRenderOption="UNFORMATTED_VALUE",
62
+ )
63
+ .execute()
64
+ )
65
+
66
+ values = result.get("values", [])
67
+ if not values:
68
+ return pd.DataFrame()
69
+
70
+ if header:
71
+ return pd.DataFrame(values[1:], columns=values[0])
72
+
73
+ return pd.DataFrame(values)
74
+
75
+ # Sheets — APPEND
76
+ # ------------------------------------------------------------------
77
+ def append_dataframe(
78
+ self,
79
+ spreadsheet_id: str,
80
+ range_: str,
81
+ df: pd.DataFrame,
82
+ ) -> None:
83
+ self.append_rows(
84
+ spreadsheet_id,
85
+ range_,
86
+ df.astype(object).where(pd.notnull(df), "").values.tolist(),
87
+ )
88
+
89
+
90
+ type GoogleSheetClient = GoogleSheetDF
91
+
92
+
93
+ class GGSheetOnlineLicenseSystemDF(GGSheetOnlineLicenseSystem):
94
+ def __init__(self, google_client: GoogleSheetClient, sheet_id: str, sheet_name: str = "Sheet1") -> None:
95
+ """
96
+ Google sheet online license system instance
97
+
98
+ Parameters
99
+ ----------
100
+ google_client : GoogleSheetClient
101
+ Google sheet client
102
+
103
+ sheet_id : str
104
+ Sheet ID
105
+
106
+ sheet_name : str, optional
107
+ Sheet name, by default ``"Sheet1"``
108
+ """
109
+ self.client = google_client
110
+ self._sheet_id = sheet_id
111
+ self._sheet_name = sheet_name
112
+
113
+ # Variable
114
+ self._df: pd.DataFrame | None = None
115
+ try:
116
+ self._load()
117
+ except Exception:
118
+ pass
119
+
120
+ # Support
121
+ @override
122
+ def _load(self) -> None:
123
+ df = self.client.read_sheet_as_dataframe(spreadsheet_id=self._sheet_id, sheet_name=self._sheet_name)
124
+ self._df = df
125
+
126
+ @override
127
+ def _make_device_entry(self) -> list:
128
+ df = self._df
129
+ row = [int(df[DF_COL.TT].max()) + 1]
130
+ row.extend(self._gather_system_info())
131
+ row.extend([datetime.now().strftime("%d/%m/%Y"), False])
132
+ return row
133
+
134
+ @override
135
+ def _get_device_online(self):
136
+ """Get ID of device in sheet, create entry if None"""
137
+ df = self._df
138
+ id = df[df[DF_COL.HWID] == self.hwid][DF_COL.TT]
139
+
140
+ has_updated = False # safe guard
141
+ if id is None or id.empty:
142
+ if not has_updated:
143
+ new_entry = self._make_device_entry()
144
+ self.client.append_rows(self._sheet_id, self._sheet_name, [new_entry])
145
+ has_updated = True
146
+ self._load()
147
+ return new_entry[0]
148
+
149
+ return id.to_list()[0]
150
+
151
+ # Main
152
+ @override
153
+ def license_check(self) -> None:
154
+ """Check if activated in sheet"""
155
+ df = self._df
156
+ id = self._get_device_online()
157
+
158
+ try:
159
+ active_status = df[df[DF_COL.TT] == id][DF_COL.ACTIVE].to_list()[0]
160
+ if not active_status:
161
+ raise SystemExit("Not activated")
162
+ return None
163
+ except Exception:
164
+ raise SystemExit("Not activated")
165
+
166
+ # Classmethod
167
+ @classmethod
168
+ def from_service_account(
169
+ cls, service_account_data: str | Path | dict[str, str], sheet_id: str, sheet_name: str = "Sheet1"
170
+ ) -> Self:
171
+ """
172
+ Google sheet online license system instance
173
+
174
+ Parameters
175
+ ----------
176
+ service_account_data : str | Path | dict[str, str]
177
+ Path to ``service_account.json`` file or .json loaded data
178
+
179
+ sheet_id : str
180
+ Sheet ID
181
+
182
+ sheet_name : str, optional
183
+ Sheet name, by default ``"Sheet1"``
184
+ """
185
+ client = GoogleSheetDF(service_account_data)
186
+ return cls(client, sheet_id, sheet_name)