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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eapyTool
3
- Version: 0.3.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "eapyTool"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "A Python utility library for math, algebra, data structures, and everyday tools"
9
9
  readme = "README.md"
10
10
  license = "MPL-2.0"
@@ -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.2"
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