absfuyu 5.6.1__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.
- absfuyu/__init__.py +5 -3
- absfuyu/__main__.py +2 -2
- absfuyu/cli/__init__.py +13 -2
- absfuyu/cli/audio_group.py +98 -0
- absfuyu/cli/color.py +2 -2
- absfuyu/cli/config_group.py +2 -2
- absfuyu/cli/do_group.py +2 -2
- absfuyu/cli/game_group.py +20 -2
- absfuyu/cli/tool_group.py +68 -4
- absfuyu/config/__init__.py +3 -3
- absfuyu/core/__init__.py +10 -6
- absfuyu/core/baseclass.py +104 -34
- absfuyu/core/baseclass2.py +43 -2
- absfuyu/core/decorator.py +2 -2
- absfuyu/core/docstring.py +4 -2
- absfuyu/core/dummy_cli.py +3 -3
- absfuyu/core/dummy_func.py +2 -2
- absfuyu/dxt/__init__.py +2 -2
- absfuyu/dxt/base_type.py +93 -0
- absfuyu/dxt/dictext.py +188 -6
- absfuyu/dxt/dxt_support.py +2 -2
- absfuyu/dxt/intext.py +72 -4
- absfuyu/dxt/listext.py +495 -23
- absfuyu/dxt/strext.py +2 -2
- absfuyu/extra/__init__.py +2 -2
- absfuyu/extra/audio/__init__.py +8 -0
- absfuyu/extra/audio/_util.py +57 -0
- absfuyu/extra/audio/convert.py +192 -0
- absfuyu/extra/audio/lossless.py +281 -0
- absfuyu/extra/beautiful.py +2 -2
- absfuyu/extra/da/__init__.py +39 -3
- absfuyu/extra/da/dadf.py +436 -29
- absfuyu/extra/da/dadf_base.py +2 -2
- absfuyu/extra/da/df_func.py +89 -5
- absfuyu/extra/da/mplt.py +2 -2
- absfuyu/extra/ggapi/__init__.py +8 -0
- absfuyu/extra/ggapi/gdrive.py +223 -0
- absfuyu/extra/ggapi/glicense.py +148 -0
- absfuyu/extra/ggapi/glicense_df.py +186 -0
- absfuyu/extra/ggapi/gsheet.py +88 -0
- absfuyu/extra/img/__init__.py +30 -0
- absfuyu/extra/img/converter.py +402 -0
- absfuyu/extra/img/dup_check.py +291 -0
- absfuyu/extra/pdf.py +4 -6
- absfuyu/extra/rclone.py +253 -0
- absfuyu/extra/xml.py +90 -0
- absfuyu/fun/__init__.py +2 -20
- absfuyu/fun/rubik.py +2 -2
- absfuyu/fun/tarot.py +2 -2
- absfuyu/game/__init__.py +2 -2
- absfuyu/game/game_stat.py +2 -2
- absfuyu/game/schulte.py +78 -0
- absfuyu/game/sudoku.py +2 -2
- absfuyu/game/tictactoe.py +2 -2
- absfuyu/game/wordle.py +6 -4
- absfuyu/general/__init__.py +2 -2
- absfuyu/general/content.py +2 -2
- absfuyu/general/human.py +2 -2
- absfuyu/general/resrel.py +213 -0
- absfuyu/general/shape.py +3 -8
- absfuyu/general/tax.py +344 -0
- absfuyu/logger.py +806 -59
- absfuyu/numbers/__init__.py +13 -0
- absfuyu/numbers/number_to_word.py +321 -0
- absfuyu/numbers/shorten_number.py +303 -0
- absfuyu/numbers/time_duration.py +217 -0
- absfuyu/pkg_data/__init__.py +2 -2
- absfuyu/pkg_data/deprecated.py +2 -2
- absfuyu/pkg_data/logo.py +1462 -0
- absfuyu/sort.py +4 -4
- absfuyu/tools/__init__.py +2 -2
- absfuyu/tools/checksum.py +119 -4
- absfuyu/tools/converter.py +2 -2
- absfuyu/tools/generator.py +24 -7
- absfuyu/tools/inspector.py +2 -2
- absfuyu/tools/keygen.py +2 -2
- absfuyu/tools/obfuscator.py +2 -2
- absfuyu/tools/passwordlib.py +2 -2
- absfuyu/tools/shutdownizer.py +3 -8
- absfuyu/tools/sw.py +213 -10
- absfuyu/tools/web.py +10 -13
- absfuyu/typings.py +5 -8
- absfuyu/util/__init__.py +31 -2
- absfuyu/util/api.py +7 -4
- absfuyu/util/cli.py +119 -0
- absfuyu/util/gui.py +91 -0
- absfuyu/util/json_method.py +2 -2
- absfuyu/util/lunar.py +2 -2
- absfuyu/util/package.py +124 -0
- absfuyu/util/path.py +313 -4
- absfuyu/util/performance.py +2 -2
- absfuyu/util/shorten_number.py +206 -13
- absfuyu/util/text_table.py +2 -2
- absfuyu/util/zipped.py +2 -2
- absfuyu/version.py +22 -19
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/METADATA +37 -8
- absfuyu-6.1.2.dist-info/RECORD +105 -0
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/WHEEL +1 -1
- absfuyu/extra/data_analysis.py +0 -21
- absfuyu-5.6.1.dist-info/RECORD +0 -79
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/entry_points.txt +0 -0
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/licenses/LICENSE +0 -0
absfuyu/extra/da/df_func.py
CHANGED
|
@@ -3,22 +3,33 @@ Absfuyu: Data Analysis
|
|
|
3
3
|
----------------------
|
|
4
4
|
DF Function
|
|
5
5
|
|
|
6
|
-
Version:
|
|
7
|
-
Date updated: 12/
|
|
6
|
+
Version: 6.1.1
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
# Module level
|
|
11
11
|
# ---------------------------------------------------------------------------
|
|
12
|
-
__all__ = [
|
|
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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Absfuyu: Google related
|
|
3
|
+
-----------------------
|
|
4
|
+
Google Drive downloader
|
|
5
|
+
|
|
6
|
+
Version: 6.1.1
|
|
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.1
|
|
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.1
|
|
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)
|