eapyTool 0.3.2__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.2 → eapytool-0.3.3}/PKG-INFO +31 -1
- {eapytool-0.3.2 → eapytool-0.3.3}/README.md +30 -0
- {eapytool-0.3.2 → eapytool-0.3.3}/pyproject.toml +1 -1
- {eapytool-0.3.2 → eapytool-0.3.3}/src/eapy/__init__.py +15 -1
- {eapytool-0.3.2 → eapytool-0.3.3}/src/eapy/core.py +244 -1
- {eapytool-0.3.2 → eapytool-0.3.3}/tests/test_core.py +139 -0
- {eapytool-0.3.2 → eapytool-0.3.3}/.gitignore +0 -0
- {eapytool-0.3.2 → eapytool-0.3.3}/LICENSE +0 -0
- {eapytool-0.3.2 → eapytool-0.3.3}/src/eapy/py.typed +0 -0
- {eapytool-0.3.2 → eapytool-0.3.3}/tests/__init__.py +0 -0
- {eapytool-0.3.2 → 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
|
|
@@ -152,6 +152,36 @@ A Python utility library that extends the standard library with math, algebra, d
|
|
|
152
152
|
- `colorText(text, color, style, bg)` — return colored text string
|
|
153
153
|
- `printColor(text, color, style, bg)` — print colored text
|
|
154
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
|
+
|
|
155
185
|
## Installation
|
|
156
186
|
|
|
157
187
|
```bash
|
|
@@ -129,6 +129,36 @@ A Python utility library that extends the standard library with math, algebra, d
|
|
|
129
129
|
- `colorText(text, color, style, bg)` — return colored text string
|
|
130
130
|
- `printColor(text, color, style, bg)` — print colored text
|
|
131
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
|
+
|
|
132
162
|
## Installation
|
|
133
163
|
|
|
134
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
|
|
@@ -92,4 +92,18 @@ from eapy.core import (
|
|
|
92
92
|
Storage,
|
|
93
93
|
# Colored text
|
|
94
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,
|
|
95
109
|
)
|
|
@@ -2247,4 +2247,247 @@ def colorText(text: str, color: str = "", style: str = "", bg: str = "") -> str:
|
|
|
2247
2247
|
|
|
2248
2248
|
def printColor(text: str, color: str = "", style: str = "", bg: str = ""):
|
|
2249
2249
|
"""Print colored text to terminal."""
|
|
2250
|
-
print(colorText(text, color, style, bg))
|
|
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)
|
|
@@ -37,6 +37,12 @@ from eapy.core import (
|
|
|
37
37
|
dotProduct, crossProduct, normalize,
|
|
38
38
|
Storage,
|
|
39
39
|
Color, colorText, printColor,
|
|
40
|
+
Logger, logTime,
|
|
41
|
+
Config,
|
|
42
|
+
isPhoneNumber, isDate, isHexColor,
|
|
43
|
+
listFiles, fileSize, ensureDir,
|
|
44
|
+
encrypt, decrypt,
|
|
45
|
+
formatDuration, timestamp, parseDate,
|
|
40
46
|
)
|
|
41
47
|
from math import pi
|
|
42
48
|
import os
|
|
@@ -750,3 +756,136 @@ class TestColor:
|
|
|
750
756
|
printColor("test", Color.YELLOW)
|
|
751
757
|
captured = capsys.readouterr()
|
|
752
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
|