eapyTool 0.3.1__tar.gz → 0.3.3__tar.gz
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.
- {eapytool-0.3.1 → eapytool-0.3.3}/PKG-INFO +36 -1
- {eapytool-0.3.1 → eapytool-0.3.3}/README.md +35 -0
- {eapytool-0.3.1 → eapytool-0.3.3}/pyproject.toml +1 -1
- {eapytool-0.3.1 → eapytool-0.3.3}/src/eapy/__init__.py +17 -1
- {eapytool-0.3.1 → eapytool-0.3.3}/src/eapy/core.py +303 -1
- {eapytool-0.3.1 → eapytool-0.3.3}/tests/test_core.py +164 -0
- {eapytool-0.3.1 → eapytool-0.3.3}/.gitignore +0 -0
- {eapytool-0.3.1 → eapytool-0.3.3}/LICENSE +0 -0
- {eapytool-0.3.1 → eapytool-0.3.3}/src/eapy/py.typed +0 -0
- {eapytool-0.3.1 → eapytool-0.3.3}/tests/__init__.py +0 -0
- {eapytool-0.3.1 → eapytool-0.3.3}/tests/conftest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eapyTool
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: A Python utility library for math, algebra, data structures, and everyday tools
|
|
5
5
|
Project-URL: Homepage, https://github.com/eapy/eapy
|
|
6
6
|
Project-URL: Repository, https://github.com/eapy/eapy
|
|
@@ -147,6 +147,41 @@ A Python utility library that extends the standard library with math, algebra, d
|
|
|
147
147
|
### Local Storage
|
|
148
148
|
- `Storage` — persistent key-value store with subpath support
|
|
149
149
|
|
|
150
|
+
### Colored Text
|
|
151
|
+
- `Color` — ANSI color codes (foreground, background, styles)
|
|
152
|
+
- `colorText(text, color, style, bg)` — return colored text string
|
|
153
|
+
- `printColor(text, color, style, bg)` — print colored text
|
|
154
|
+
|
|
155
|
+
### Logging
|
|
156
|
+
- `Logger` — simple logger (console + file)
|
|
157
|
+
- `@logTime` — log function calls with timing
|
|
158
|
+
|
|
159
|
+
### Config Management
|
|
160
|
+
- `Config` — JSON-based config with dot notation support
|
|
161
|
+
|
|
162
|
+
### Validation (Extended)
|
|
163
|
+
- `isPhoneNumber` — Chinese phone number validation
|
|
164
|
+
- `isDate` — date format validation
|
|
165
|
+
- `isHexColor` — hex color validation
|
|
166
|
+
|
|
167
|
+
### File Utilities
|
|
168
|
+
- `listFiles(dir, ext)` — list files by extension
|
|
169
|
+
- `fileSize(path)` — human-readable file size
|
|
170
|
+
- `ensureDir(path)` — create directory if not exists
|
|
171
|
+
|
|
172
|
+
### CLI Interaction
|
|
173
|
+
- `confirm(prompt)` — yes/no confirmation
|
|
174
|
+
- `askInput(prompt, default)` — input with default
|
|
175
|
+
- `selectOption(prompt, options)` — selection menu
|
|
176
|
+
|
|
177
|
+
### Encryption
|
|
178
|
+
- `encrypt(text, key)` / `decrypt(text, key)` — simple XOR encryption
|
|
179
|
+
|
|
180
|
+
### Time Utilities
|
|
181
|
+
- `formatDuration(seconds)` — human-readable duration
|
|
182
|
+
- `timestamp()` — current Unix timestamp
|
|
183
|
+
- `parseDate(str, fmt)` — parse date string
|
|
184
|
+
|
|
150
185
|
## Installation
|
|
151
186
|
|
|
152
187
|
```bash
|
|
@@ -124,6 +124,41 @@ A Python utility library that extends the standard library with math, algebra, d
|
|
|
124
124
|
### Local Storage
|
|
125
125
|
- `Storage` — persistent key-value store with subpath support
|
|
126
126
|
|
|
127
|
+
### Colored Text
|
|
128
|
+
- `Color` — ANSI color codes (foreground, background, styles)
|
|
129
|
+
- `colorText(text, color, style, bg)` — return colored text string
|
|
130
|
+
- `printColor(text, color, style, bg)` — print colored text
|
|
131
|
+
|
|
132
|
+
### Logging
|
|
133
|
+
- `Logger` — simple logger (console + file)
|
|
134
|
+
- `@logTime` — log function calls with timing
|
|
135
|
+
|
|
136
|
+
### Config Management
|
|
137
|
+
- `Config` — JSON-based config with dot notation support
|
|
138
|
+
|
|
139
|
+
### Validation (Extended)
|
|
140
|
+
- `isPhoneNumber` — Chinese phone number validation
|
|
141
|
+
- `isDate` — date format validation
|
|
142
|
+
- `isHexColor` — hex color validation
|
|
143
|
+
|
|
144
|
+
### File Utilities
|
|
145
|
+
- `listFiles(dir, ext)` — list files by extension
|
|
146
|
+
- `fileSize(path)` — human-readable file size
|
|
147
|
+
- `ensureDir(path)` — create directory if not exists
|
|
148
|
+
|
|
149
|
+
### CLI Interaction
|
|
150
|
+
- `confirm(prompt)` — yes/no confirmation
|
|
151
|
+
- `askInput(prompt, default)` — input with default
|
|
152
|
+
- `selectOption(prompt, options)` — selection menu
|
|
153
|
+
|
|
154
|
+
### Encryption
|
|
155
|
+
- `encrypt(text, key)` / `decrypt(text, key)` — simple XOR encryption
|
|
156
|
+
|
|
157
|
+
### Time Utilities
|
|
158
|
+
- `formatDuration(seconds)` — human-readable duration
|
|
159
|
+
- `timestamp()` — current Unix timestamp
|
|
160
|
+
- `parseDate(str, fmt)` — parse date string
|
|
161
|
+
|
|
127
162
|
## Installation
|
|
128
163
|
|
|
129
164
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""eapy - A Python utility library for math, algebra, data structures, and everyday tools."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.3.
|
|
3
|
+
__version__ = "0.3.3"
|
|
4
4
|
|
|
5
5
|
from eapy.core import (
|
|
6
6
|
# Timer
|
|
@@ -90,4 +90,20 @@ from eapy.core import (
|
|
|
90
90
|
dotProduct, crossProduct, normalize,
|
|
91
91
|
# Local storage
|
|
92
92
|
Storage,
|
|
93
|
+
# Colored text
|
|
94
|
+
Color, colorText, printColor,
|
|
95
|
+
# Logging
|
|
96
|
+
Logger, logTime,
|
|
97
|
+
# Config
|
|
98
|
+
Config,
|
|
99
|
+
# Validation (extended)
|
|
100
|
+
isPhoneNumber, isDate, isHexColor,
|
|
101
|
+
# File utilities
|
|
102
|
+
listFiles, fileSize, ensureDir,
|
|
103
|
+
# CLI interaction
|
|
104
|
+
confirm, askInput, selectOption,
|
|
105
|
+
# Encryption
|
|
106
|
+
encrypt, decrypt,
|
|
107
|
+
# Time utilities
|
|
108
|
+
formatDuration, timestamp, parseDate,
|
|
93
109
|
)
|
|
@@ -2188,4 +2188,306 @@ class Storage:
|
|
|
2188
2188
|
os.makedirs(self.base_path, exist_ok=True)
|
|
2189
2189
|
|
|
2190
2190
|
def __repr__(self):
|
|
2191
|
-
return f"Storage(base_path='{self.base_path}')"
|
|
2191
|
+
return f"Storage(base_path='{self.base_path}')"
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
# ======================================================== Colored text ========================================================
|
|
2195
|
+
|
|
2196
|
+
class Color:
|
|
2197
|
+
"""ANSI color codes for terminal output."""
|
|
2198
|
+
RESET = "\033[0m"
|
|
2199
|
+
# Foreground colors
|
|
2200
|
+
BLACK = "\033[30m"
|
|
2201
|
+
RED = "\033[31m"
|
|
2202
|
+
GREEN = "\033[32m"
|
|
2203
|
+
YELLOW = "\033[33m"
|
|
2204
|
+
BLUE = "\033[34m"
|
|
2205
|
+
MAGENTA = "\033[35m"
|
|
2206
|
+
CYAN = "\033[36m"
|
|
2207
|
+
WHITE = "\033[37m"
|
|
2208
|
+
# Bright foreground
|
|
2209
|
+
BRIGHT_RED = "\033[91m"
|
|
2210
|
+
BRIGHT_GREEN = "\033[92m"
|
|
2211
|
+
BRIGHT_YELLOW = "\033[93m"
|
|
2212
|
+
BRIGHT_BLUE = "\033[94m"
|
|
2213
|
+
BRIGHT_MAGENTA = "\033[95m"
|
|
2214
|
+
BRIGHT_CYAN = "\033[96m"
|
|
2215
|
+
BRIGHT_WHITE = "\033[97m"
|
|
2216
|
+
# Styles
|
|
2217
|
+
BOLD = "\033[1m"
|
|
2218
|
+
DIM = "\033[2m"
|
|
2219
|
+
ITALIC = "\033[3m"
|
|
2220
|
+
UNDERLINE = "\033[4m"
|
|
2221
|
+
# Background colors
|
|
2222
|
+
BG_BLACK = "\033[40m"
|
|
2223
|
+
BG_RED = "\033[41m"
|
|
2224
|
+
BG_GREEN = "\033[42m"
|
|
2225
|
+
BG_YELLOW = "\033[43m"
|
|
2226
|
+
BG_BLUE = "\033[44m"
|
|
2227
|
+
BG_MAGENTA = "\033[45m"
|
|
2228
|
+
BG_CYAN = "\033[46m"
|
|
2229
|
+
BG_WHITE = "\033[47m"
|
|
2230
|
+
|
|
2231
|
+
|
|
2232
|
+
def colorText(text: str, color: str = "", style: str = "", bg: str = "") -> str:
|
|
2233
|
+
"""Return text wrapped with ANSI color codes.
|
|
2234
|
+
|
|
2235
|
+
Args:
|
|
2236
|
+
text: The text to colorize.
|
|
2237
|
+
color: Foreground color (e.g., Color.RED, Color.BLUE).
|
|
2238
|
+
style: Style (e.g., Color.BOLD, Color.UNDERLINE).
|
|
2239
|
+
bg: Background color (e.g., Color.BG_RED).
|
|
2240
|
+
|
|
2241
|
+
Returns:
|
|
2242
|
+
Colored text string.
|
|
2243
|
+
"""
|
|
2244
|
+
codes = style + bg + color
|
|
2245
|
+
return f"{codes}{text}{Color.RESET}"
|
|
2246
|
+
|
|
2247
|
+
|
|
2248
|
+
def printColor(text: str, color: str = "", style: str = "", bg: str = ""):
|
|
2249
|
+
"""Print colored text to terminal."""
|
|
2250
|
+
print(colorText(text, color, style, bg))
|
|
2251
|
+
|
|
2252
|
+
|
|
2253
|
+
# ======================================================== Logging ========================================================
|
|
2254
|
+
|
|
2255
|
+
import logging as _logging
|
|
2256
|
+
|
|
2257
|
+
|
|
2258
|
+
class Logger:
|
|
2259
|
+
"""Simple logger that outputs to both console and file."""
|
|
2260
|
+
|
|
2261
|
+
def __init__(self, name: str = "app", file_path: str = None, level: int = _logging.INFO):
|
|
2262
|
+
self._logger = _logging.getLogger(name)
|
|
2263
|
+
self._logger.setLevel(level)
|
|
2264
|
+
self._logger.handlers.clear()
|
|
2265
|
+
fmt = _logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
|
2266
|
+
ch = _logging.StreamHandler()
|
|
2267
|
+
ch.setFormatter(fmt)
|
|
2268
|
+
self._logger.addHandler(ch)
|
|
2269
|
+
if file_path:
|
|
2270
|
+
fh = _logging.FileHandler(file_path, encoding="utf-8")
|
|
2271
|
+
fh.setFormatter(fmt)
|
|
2272
|
+
self._logger.addHandler(fh)
|
|
2273
|
+
|
|
2274
|
+
def info(self, msg: str):
|
|
2275
|
+
self._logger.info(msg)
|
|
2276
|
+
|
|
2277
|
+
def debug(self, msg: str):
|
|
2278
|
+
self._logger.debug(msg)
|
|
2279
|
+
|
|
2280
|
+
def warning(self, msg: str):
|
|
2281
|
+
self._logger.warning(msg)
|
|
2282
|
+
|
|
2283
|
+
def error(self, msg: str):
|
|
2284
|
+
self._logger.error(msg)
|
|
2285
|
+
|
|
2286
|
+
def critical(self, msg: str):
|
|
2287
|
+
self._logger.critical(msg)
|
|
2288
|
+
|
|
2289
|
+
|
|
2290
|
+
def logTime(func):
|
|
2291
|
+
"""Decorator that logs function calls with arguments and return value."""
|
|
2292
|
+
@functools.wraps(func)
|
|
2293
|
+
def wrapper(*args, **kwargs):
|
|
2294
|
+
arg_str = ", ".join([str(a) for a in args] + [f"{k}={v}" for k, v in kwargs.items()])
|
|
2295
|
+
start = _time.perf_counter()
|
|
2296
|
+
result = func(*args, **kwargs)
|
|
2297
|
+
elapsed = _time.perf_counter() - start
|
|
2298
|
+
print(f"[LOG] {func.__name__}({arg_str}) -> {result} ({elapsed:.4f}s)")
|
|
2299
|
+
return result
|
|
2300
|
+
return wrapper
|
|
2301
|
+
|
|
2302
|
+
|
|
2303
|
+
# ======================================================== Config ========================================================
|
|
2304
|
+
|
|
2305
|
+
class Config:
|
|
2306
|
+
"""Simple configuration manager with JSON persistence."""
|
|
2307
|
+
|
|
2308
|
+
def __init__(self, path: str = "config.json"):
|
|
2309
|
+
self.path = path
|
|
2310
|
+
self._data = {}
|
|
2311
|
+
if os.path.exists(path):
|
|
2312
|
+
self._data = readJson(path)
|
|
2313
|
+
|
|
2314
|
+
def get(self, key: str, default=None):
|
|
2315
|
+
"""Get a config value. Supports dot notation (e.g., 'db.host')."""
|
|
2316
|
+
keys = key.split(".")
|
|
2317
|
+
val = self._data
|
|
2318
|
+
for k in keys:
|
|
2319
|
+
if isinstance(val, dict) and k in val:
|
|
2320
|
+
val = val[k]
|
|
2321
|
+
else:
|
|
2322
|
+
return default
|
|
2323
|
+
return val
|
|
2324
|
+
|
|
2325
|
+
def set(self, key: str, value):
|
|
2326
|
+
"""Set a config value. Supports dot notation."""
|
|
2327
|
+
keys = key.split(".")
|
|
2328
|
+
d = self._data
|
|
2329
|
+
for k in keys[:-1]:
|
|
2330
|
+
if k not in d or not isinstance(d[k], dict):
|
|
2331
|
+
d[k] = {}
|
|
2332
|
+
d = d[k]
|
|
2333
|
+
d[keys[-1]] = value
|
|
2334
|
+
|
|
2335
|
+
def delete(self, key: str):
|
|
2336
|
+
"""Delete a config key."""
|
|
2337
|
+
keys = key.split(".")
|
|
2338
|
+
d = self._data
|
|
2339
|
+
for k in keys[:-1]:
|
|
2340
|
+
if isinstance(d, dict) and k in d:
|
|
2341
|
+
d = d[k]
|
|
2342
|
+
else:
|
|
2343
|
+
return
|
|
2344
|
+
d.pop(keys[-1], None)
|
|
2345
|
+
|
|
2346
|
+
def save(self):
|
|
2347
|
+
"""Save config to file."""
|
|
2348
|
+
writeJson(self.path, self._data)
|
|
2349
|
+
|
|
2350
|
+
def reload(self):
|
|
2351
|
+
"""Reload config from file."""
|
|
2352
|
+
if os.path.exists(self.path):
|
|
2353
|
+
self._data = readJson(self.path)
|
|
2354
|
+
|
|
2355
|
+
def all(self) -> dict:
|
|
2356
|
+
"""Return all config data."""
|
|
2357
|
+
return self._data.copy()
|
|
2358
|
+
|
|
2359
|
+
def __repr__(self):
|
|
2360
|
+
return f"Config(path='{self.path}', keys={list(self._data.keys())})"
|
|
2361
|
+
|
|
2362
|
+
|
|
2363
|
+
# ======================================================== Validation (extended) ========================================================
|
|
2364
|
+
|
|
2365
|
+
def isPhoneNumber(s: str) -> bool:
|
|
2366
|
+
"""Check if string is a valid Chinese phone number."""
|
|
2367
|
+
return bool(re.match(r'^1[3-9]\d{9}$', s))
|
|
2368
|
+
|
|
2369
|
+
|
|
2370
|
+
def isDate(s: str, fmt: str = "%Y-%m-%d") -> bool:
|
|
2371
|
+
"""Check if string is a valid date in given format."""
|
|
2372
|
+
try:
|
|
2373
|
+
datetime.strptime(s, fmt)
|
|
2374
|
+
return True
|
|
2375
|
+
except ValueError:
|
|
2376
|
+
return False
|
|
2377
|
+
|
|
2378
|
+
|
|
2379
|
+
def isHexColor(s: str) -> bool:
|
|
2380
|
+
"""Check if string is a valid hex color (#RGB or #RRGGBB)."""
|
|
2381
|
+
return bool(re.match(r'^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$', s))
|
|
2382
|
+
|
|
2383
|
+
|
|
2384
|
+
# ======================================================== File utilities ========================================================
|
|
2385
|
+
|
|
2386
|
+
def listFiles(directory: str = ".", ext: str = None, recursive: bool = False) -> list[str]:
|
|
2387
|
+
"""List files in directory, optionally filtered by extension."""
|
|
2388
|
+
result = []
|
|
2389
|
+
if recursive:
|
|
2390
|
+
for root, dirs, files in os.walk(directory):
|
|
2391
|
+
for f in files:
|
|
2392
|
+
if ext is None or f.endswith(ext):
|
|
2393
|
+
result.append(os.path.join(root, f))
|
|
2394
|
+
else:
|
|
2395
|
+
for f in os.listdir(directory):
|
|
2396
|
+
full = os.path.join(directory, f)
|
|
2397
|
+
if os.path.isfile(full):
|
|
2398
|
+
if ext is None or f.endswith(ext):
|
|
2399
|
+
result.append(full)
|
|
2400
|
+
return result
|
|
2401
|
+
|
|
2402
|
+
|
|
2403
|
+
def fileSize(path: str) -> str:
|
|
2404
|
+
"""Return human-readable file size."""
|
|
2405
|
+
size = os.path.getsize(path)
|
|
2406
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
2407
|
+
if size < 1024:
|
|
2408
|
+
return f"{size:.1f} {unit}"
|
|
2409
|
+
size /= 1024
|
|
2410
|
+
return f"{size:.1f} PB"
|
|
2411
|
+
|
|
2412
|
+
|
|
2413
|
+
def ensureDir(path: str):
|
|
2414
|
+
"""Create directory if it doesn't exist."""
|
|
2415
|
+
os.makedirs(path, exist_ok=True)
|
|
2416
|
+
|
|
2417
|
+
|
|
2418
|
+
# ======================================================== CLI interaction ========================================================
|
|
2419
|
+
|
|
2420
|
+
def confirm(prompt: str, default: bool = False) -> bool:
|
|
2421
|
+
"""Ask user for yes/no confirmation."""
|
|
2422
|
+
suffix = " [Y/n] " if default else " [y/N] "
|
|
2423
|
+
answer = input(prompt + suffix).strip().lower()
|
|
2424
|
+
if not answer:
|
|
2425
|
+
return default
|
|
2426
|
+
return answer in ("y", "yes")
|
|
2427
|
+
|
|
2428
|
+
|
|
2429
|
+
def askInput(prompt: str, default: str = "") -> str:
|
|
2430
|
+
"""Ask user for input with optional default value."""
|
|
2431
|
+
if default:
|
|
2432
|
+
result = input(f"{prompt} [{default}]: ").strip()
|
|
2433
|
+
return result if result else default
|
|
2434
|
+
return input(f"{prompt}: ").strip()
|
|
2435
|
+
|
|
2436
|
+
|
|
2437
|
+
def selectOption(prompt: str, options: list[str]) -> str:
|
|
2438
|
+
"""Show a menu and let user select an option."""
|
|
2439
|
+
print(prompt)
|
|
2440
|
+
for i, opt in enumerate(options, 1):
|
|
2441
|
+
print(f" {i}. {opt}")
|
|
2442
|
+
while True:
|
|
2443
|
+
choice = input("Enter number: ").strip()
|
|
2444
|
+
if choice.isdigit() and 1 <= int(choice) <= len(options):
|
|
2445
|
+
return options[int(choice) - 1]
|
|
2446
|
+
print("Invalid choice, try again.")
|
|
2447
|
+
|
|
2448
|
+
|
|
2449
|
+
# ======================================================== Encryption ========================================================
|
|
2450
|
+
|
|
2451
|
+
def encrypt(text: str, key: str) -> str:
|
|
2452
|
+
"""Simple XOR encryption, returns hex string."""
|
|
2453
|
+
key_bytes = key.encode()
|
|
2454
|
+
text_bytes = text.encode()
|
|
2455
|
+
encrypted = bytes(t ^ key_bytes[i % len(key_bytes)] for i, t in enumerate(text_bytes))
|
|
2456
|
+
return encrypted.hex()
|
|
2457
|
+
|
|
2458
|
+
|
|
2459
|
+
def decrypt(hex_str: str, key: str) -> str:
|
|
2460
|
+
"""Decrypt XOR encrypted hex string."""
|
|
2461
|
+
key_bytes = key.encode()
|
|
2462
|
+
data = bytes.fromhex(hex_str)
|
|
2463
|
+
decrypted = bytes(d ^ key_bytes[i % len(key_bytes)] for i, d in enumerate(data))
|
|
2464
|
+
return decrypted.decode()
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
# ======================================================== Time utilities ========================================================
|
|
2468
|
+
|
|
2469
|
+
def formatDuration(seconds: float) -> str:
|
|
2470
|
+
"""Format seconds into human-readable duration."""
|
|
2471
|
+
if seconds < 60:
|
|
2472
|
+
return f"{seconds:.1f}s"
|
|
2473
|
+
minutes = int(seconds // 60)
|
|
2474
|
+
secs = seconds % 60
|
|
2475
|
+
if minutes < 60:
|
|
2476
|
+
return f"{minutes}m {secs:.0f}s"
|
|
2477
|
+
hours = minutes // 60
|
|
2478
|
+
mins = minutes % 60
|
|
2479
|
+
if hours < 24:
|
|
2480
|
+
return f"{hours}h {mins}m {secs:.0f}s"
|
|
2481
|
+
days = hours // 24
|
|
2482
|
+
hrs = hours % 24
|
|
2483
|
+
return f"{days}d {hrs}h {mins}m"
|
|
2484
|
+
|
|
2485
|
+
|
|
2486
|
+
def timestamp() -> float:
|
|
2487
|
+
"""Return current Unix timestamp."""
|
|
2488
|
+
return _time.time()
|
|
2489
|
+
|
|
2490
|
+
|
|
2491
|
+
def parseDate(date_str: str, fmt: str = "%Y-%m-%d") -> datetime:
|
|
2492
|
+
"""Parse date string into datetime object."""
|
|
2493
|
+
return datetime.strptime(date_str, fmt)
|
|
@@ -36,6 +36,13 @@ from eapy.core import (
|
|
|
36
36
|
isNumber, isString, isList, isDict, isCallable,
|
|
37
37
|
dotProduct, crossProduct, normalize,
|
|
38
38
|
Storage,
|
|
39
|
+
Color, colorText, printColor,
|
|
40
|
+
Logger, logTime,
|
|
41
|
+
Config,
|
|
42
|
+
isPhoneNumber, isDate, isHexColor,
|
|
43
|
+
listFiles, fileSize, ensureDir,
|
|
44
|
+
encrypt, decrypt,
|
|
45
|
+
formatDuration, timestamp, parseDate,
|
|
39
46
|
)
|
|
40
47
|
from math import pi
|
|
41
48
|
import os
|
|
@@ -725,3 +732,160 @@ class TestStorage:
|
|
|
725
732
|
self.store.set("x", 1)
|
|
726
733
|
self.store.clear()
|
|
727
734
|
assert self.store.exists("x") is False
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
# ======================================================== Colored text ========================================================
|
|
738
|
+
|
|
739
|
+
class TestColor:
|
|
740
|
+
def test_colorText(self):
|
|
741
|
+
result = colorText("hello", Color.RED)
|
|
742
|
+
assert Color.RED in result
|
|
743
|
+
assert Color.RESET in result
|
|
744
|
+
assert "hello" in result
|
|
745
|
+
|
|
746
|
+
def test_colorText_with_style(self):
|
|
747
|
+
result = colorText("bold", Color.GREEN, Color.BOLD)
|
|
748
|
+
assert Color.BOLD in result
|
|
749
|
+
assert Color.GREEN in result
|
|
750
|
+
|
|
751
|
+
def test_colorText_with_bg(self):
|
|
752
|
+
result = colorText("bg", bg=Color.BG_BLUE)
|
|
753
|
+
assert Color.BG_BLUE in result
|
|
754
|
+
|
|
755
|
+
def test_printColor(self, capsys):
|
|
756
|
+
printColor("test", Color.YELLOW)
|
|
757
|
+
captured = capsys.readouterr()
|
|
758
|
+
assert "test" in captured.out
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
# ======================================================== Logging ========================================================
|
|
762
|
+
|
|
763
|
+
class TestLogging:
|
|
764
|
+
def test_logger(self, capsys):
|
|
765
|
+
logger = Logger("test")
|
|
766
|
+
logger.info("hello")
|
|
767
|
+
captured = capsys.readouterr()
|
|
768
|
+
assert "hello" in captured.err
|
|
769
|
+
|
|
770
|
+
def test_logTime(self, capsys):
|
|
771
|
+
@logTime
|
|
772
|
+
def add(a, b):
|
|
773
|
+
return a + b
|
|
774
|
+
result = add(1, 2)
|
|
775
|
+
assert result == 3
|
|
776
|
+
captured = capsys.readouterr()
|
|
777
|
+
assert "add" in captured.out
|
|
778
|
+
assert "LOG" in captured.out
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
# ======================================================== Config ========================================================
|
|
782
|
+
|
|
783
|
+
class TestConfig:
|
|
784
|
+
def setup_method(self):
|
|
785
|
+
self.cfg_path = ".test_config.json"
|
|
786
|
+
if os.path.exists(self.cfg_path):
|
|
787
|
+
os.remove(self.cfg_path)
|
|
788
|
+
|
|
789
|
+
def teardown_method(self):
|
|
790
|
+
if os.path.exists(self.cfg_path):
|
|
791
|
+
os.remove(self.cfg_path)
|
|
792
|
+
|
|
793
|
+
def test_set_get(self):
|
|
794
|
+
cfg = Config(self.cfg_path)
|
|
795
|
+
cfg.set("name", "test")
|
|
796
|
+
assert cfg.get("name") == "test"
|
|
797
|
+
|
|
798
|
+
def test_dot_notation(self):
|
|
799
|
+
cfg = Config(self.cfg_path)
|
|
800
|
+
cfg.set("db.host", "localhost")
|
|
801
|
+
assert cfg.get("db.host") == "localhost"
|
|
802
|
+
|
|
803
|
+
def test_save_reload(self):
|
|
804
|
+
cfg = Config(self.cfg_path)
|
|
805
|
+
cfg.set("key", "value")
|
|
806
|
+
cfg.save()
|
|
807
|
+
cfg2 = Config(self.cfg_path)
|
|
808
|
+
assert cfg2.get("key") == "value"
|
|
809
|
+
|
|
810
|
+
def test_delete(self):
|
|
811
|
+
cfg = Config(self.cfg_path)
|
|
812
|
+
cfg.set("a", 1)
|
|
813
|
+
cfg.delete("a")
|
|
814
|
+
assert cfg.get("a") is None
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
# ======================================================== Validation (extended) ========================================================
|
|
818
|
+
|
|
819
|
+
class TestValidationExtended:
|
|
820
|
+
def test_isPhoneNumber(self):
|
|
821
|
+
assert isPhoneNumber("13812345678") is True
|
|
822
|
+
assert isPhoneNumber("12345") is False
|
|
823
|
+
|
|
824
|
+
def test_isDate(self):
|
|
825
|
+
assert isDate("2024-01-15") is True
|
|
826
|
+
assert isDate("not-a-date") is False
|
|
827
|
+
|
|
828
|
+
def test_isHexColor(self):
|
|
829
|
+
assert isHexColor("#ff0000") is True
|
|
830
|
+
assert isHexColor("#f00") is True
|
|
831
|
+
assert isHexColor("red") is False
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
# ======================================================== File utilities ========================================================
|
|
835
|
+
|
|
836
|
+
class TestFileUtils:
|
|
837
|
+
def setup_method(self):
|
|
838
|
+
self.test_dir = ".test_file_utils"
|
|
839
|
+
ensureDir(self.test_dir)
|
|
840
|
+
with open(os.path.join(self.test_dir, "test.txt"), "w") as f:
|
|
841
|
+
f.write("hello")
|
|
842
|
+
|
|
843
|
+
def teardown_method(self):
|
|
844
|
+
if os.path.exists(self.test_dir):
|
|
845
|
+
shutil.rmtree(self.test_dir)
|
|
846
|
+
|
|
847
|
+
def test_listFiles(self):
|
|
848
|
+
files = listFiles(self.test_dir, ext=".txt")
|
|
849
|
+
assert len(files) == 1
|
|
850
|
+
assert files[0].endswith(".txt")
|
|
851
|
+
|
|
852
|
+
def test_fileSize(self):
|
|
853
|
+
path = os.path.join(self.test_dir, "test.txt")
|
|
854
|
+
size = fileSize(path)
|
|
855
|
+
assert "B" in size
|
|
856
|
+
|
|
857
|
+
def test_ensureDir(self):
|
|
858
|
+
new_dir = os.path.join(self.test_dir, "sub", "dir")
|
|
859
|
+
ensureDir(new_dir)
|
|
860
|
+
assert os.path.isdir(new_dir)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
# ======================================================== Encryption ========================================================
|
|
864
|
+
|
|
865
|
+
class TestEncryption:
|
|
866
|
+
def test_encrypt_decrypt(self):
|
|
867
|
+
text = "hello world"
|
|
868
|
+
key = "secret"
|
|
869
|
+
encrypted = encrypt(text, key)
|
|
870
|
+
assert encrypted != text
|
|
871
|
+
decrypted = decrypt(encrypted, key)
|
|
872
|
+
assert decrypted == text
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
# ======================================================== Time utilities ========================================================
|
|
876
|
+
|
|
877
|
+
class TestTimeUtils:
|
|
878
|
+
def test_formatDuration(self):
|
|
879
|
+
assert formatDuration(30) == "30.0s"
|
|
880
|
+
assert formatDuration(90) == "1m 30s"
|
|
881
|
+
assert formatDuration(3661) == "1h 1m 1s"
|
|
882
|
+
|
|
883
|
+
def test_timestamp(self):
|
|
884
|
+
ts = timestamp()
|
|
885
|
+
assert ts > 0
|
|
886
|
+
|
|
887
|
+
def test_parseDate(self):
|
|
888
|
+
dt = parseDate("2024-01-15")
|
|
889
|
+
assert dt.year == 2024
|
|
890
|
+
assert dt.month == 1
|
|
891
|
+
assert dt.day == 15
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|