eapyTool 0.3.0__tar.gz → 0.3.2__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.0
3
+ Version: 0.3.2
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
@@ -127,6 +127,31 @@ A Python utility library that extends the standard library with math, algebra, d
127
127
  - `extractEmails`, `extractURLs` — extract from text
128
128
  - `replaceMultiple` — multi-pattern replacement
129
129
 
130
+ ### Cache & Performance
131
+ - `@lruCache(maxsize)` — LRU cache decorator
132
+ - `@timeit` — execution time decorator
133
+
134
+ ### Progress Iterator
135
+ - `tqdm(iterable)` — progress bar iterator
136
+
137
+ ### Type Checks
138
+ - `isNumber`, `isString`, `isList`, `isDict`, `isCallable` — type check helpers
139
+
140
+ ### Network Utilities
141
+ - `getJson(url)` — GET request returning JSON
142
+ - `postJson(url, data)` — POST request with JSON body
143
+
144
+ ### Math Extensions (Vector)
145
+ - `dotProduct`, `crossProduct`, `normalize` — vector operations
146
+
147
+ ### Local Storage
148
+ - `Storage` — persistent key-value store with subpath support
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
+
130
155
  ## Installation
131
156
 
132
157
  ```bash
@@ -104,6 +104,31 @@ A Python utility library that extends the standard library with math, algebra, d
104
104
  - `extractEmails`, `extractURLs` — extract from text
105
105
  - `replaceMultiple` — multi-pattern replacement
106
106
 
107
+ ### Cache & Performance
108
+ - `@lruCache(maxsize)` — LRU cache decorator
109
+ - `@timeit` — execution time decorator
110
+
111
+ ### Progress Iterator
112
+ - `tqdm(iterable)` — progress bar iterator
113
+
114
+ ### Type Checks
115
+ - `isNumber`, `isString`, `isList`, `isDict`, `isCallable` — type check helpers
116
+
117
+ ### Network Utilities
118
+ - `getJson(url)` — GET request returning JSON
119
+ - `postJson(url, data)` — POST request with JSON body
120
+
121
+ ### Math Extensions (Vector)
122
+ - `dotProduct`, `crossProduct`, `normalize` — vector operations
123
+
124
+ ### Local Storage
125
+ - `Storage` — persistent key-value store with subpath support
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
+
107
132
  ## Installation
108
133
 
109
134
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "eapyTool"
7
- version = "0.3.0"
7
+ version = "0.3.2"
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.0"
3
+ __version__ = "0.3.2"
4
4
 
5
5
  from eapy.core import (
6
6
  # Timer
@@ -78,4 +78,18 @@ from eapy.core import (
78
78
  factorial, matrixInverse, interpolate,
79
79
  # Regex utilities
80
80
  extractEmails, extractURLs, replaceMultiple,
81
+ # Cache / Performance
82
+ lruCache, timeit,
83
+ # Progress iterator
84
+ tqdm,
85
+ # Type checks
86
+ isNumber, isString, isList, isDict, isCallable,
87
+ # Network
88
+ getJson, postJson,
89
+ # Math extensions (vector)
90
+ dotProduct, crossProduct, normalize,
91
+ # Local storage
92
+ Storage,
93
+ # Colored text
94
+ Color, colorText, printColor,
81
95
  )
@@ -1961,4 +1961,290 @@ def replaceMultiple(text: str, replacements: dict[str, str]) -> str:
1961
1961
  """Replace multiple patterns in text. Keys are patterns, values are replacements."""
1962
1962
  for pattern, replacement in replacements.items():
1963
1963
  text = text.replace(pattern, replacement)
1964
- return text
1964
+ return text
1965
+
1966
+
1967
+ # ======================================================== Cache / Performance ========================================================
1968
+
1969
+ import functools
1970
+ import time as _time
1971
+
1972
+
1973
+ def lruCache(maxsize: int = 128):
1974
+ """LRU cache decorator with max size limit."""
1975
+ def decorator(func):
1976
+ cache = {}
1977
+ order = []
1978
+
1979
+ @functools.wraps(func)
1980
+ def wrapper(*args, **kwargs):
1981
+ key = (args, tuple(sorted(kwargs.items())))
1982
+ if key in cache:
1983
+ order.remove(key)
1984
+ order.append(key)
1985
+ return cache[key]
1986
+ result = func(*args, **kwargs)
1987
+ cache[key] = result
1988
+ order.append(key)
1989
+ if len(cache) > maxsize:
1990
+ oldest = order.pop(0)
1991
+ del cache[oldest]
1992
+ return result
1993
+
1994
+ wrapper.cache_clear = lambda: (cache.clear(), order.clear())
1995
+ return wrapper
1996
+ return decorator
1997
+
1998
+
1999
+ def timeit(func):
2000
+ """Decorator that prints function execution time."""
2001
+ @functools.wraps(func)
2002
+ def wrapper(*args, **kwargs):
2003
+ start = _time.perf_counter()
2004
+ result = func(*args, **kwargs)
2005
+ elapsed = _time.perf_counter() - start
2006
+ print(f"{func.__name__} executed in {elapsed:.4f}s")
2007
+ return result
2008
+ return wrapper
2009
+
2010
+
2011
+ # ======================================================== Progress iterator ========================================================
2012
+
2013
+ class tqdm:
2014
+ """Progress bar iterator (like tqdm)."""
2015
+
2016
+ def __init__(self, iterable, total: int = None, width: int = 40, prefix: str = ""):
2017
+ self.iterable = iterable
2018
+ self.total = total if total is not None else len(iterable)
2019
+ self.width = width
2020
+ self.prefix = prefix
2021
+ self.n = 0
2022
+
2023
+ def __iter__(self):
2024
+ for item in self.iterable:
2025
+ yield item
2026
+ self.n += 1
2027
+ self._render()
2028
+
2029
+ def _render(self):
2030
+ progress = self.n / self.total if self.total else 1
2031
+ filled = int(self.width * progress)
2032
+ bar = "█" * filled + "-" * (self.width - filled)
2033
+ pct = progress * 100
2034
+ sys.stdout.write(f"\r{self.prefix}[{bar}] {pct:.1f}% ({self.n}/{self.total})")
2035
+ sys.stdout.flush()
2036
+ if self.n >= self.total:
2037
+ sys.stdout.write("\n")
2038
+
2039
+
2040
+ # ======================================================== Type checks ========================================================
2041
+
2042
+ def isNumber(value) -> bool:
2043
+ """Check if value is a number (int or float)."""
2044
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
2045
+
2046
+
2047
+ def isString(value) -> bool:
2048
+ """Check if value is a string."""
2049
+ return isinstance(value, str)
2050
+
2051
+
2052
+ def isList(value) -> bool:
2053
+ """Check if value is a list."""
2054
+ return isinstance(value, list)
2055
+
2056
+
2057
+ def isDict(value) -> bool:
2058
+ """Check if value is a dict."""
2059
+ return isinstance(value, dict)
2060
+
2061
+
2062
+ def isCallable(value) -> bool:
2063
+ """Check if value is callable."""
2064
+ return callable(value)
2065
+
2066
+
2067
+ # ======================================================== Network utilities ========================================================
2068
+
2069
+ import urllib.request
2070
+
2071
+
2072
+ def getJson(url: str, headers: dict = None):
2073
+ """Send GET request and return parsed JSON."""
2074
+ req = urllib.request.Request(url)
2075
+ if headers:
2076
+ for k, v in headers.items():
2077
+ req.add_header(k, v)
2078
+ with urllib.request.urlopen(req) as resp:
2079
+ return json.loads(resp.read().decode())
2080
+
2081
+
2082
+ def postJson(url: str, data=None, headers: dict = None):
2083
+ """Send POST request with JSON body and return parsed JSON response."""
2084
+ body = json.dumps(data).encode() if data else b"{}"
2085
+ req = urllib.request.Request(url, data=body, method="POST")
2086
+ req.add_header("Content-Type", "application/json")
2087
+ if headers:
2088
+ for k, v in headers.items():
2089
+ req.add_header(k, v)
2090
+ with urllib.request.urlopen(req) as resp:
2091
+ return json.loads(resp.read().decode())
2092
+
2093
+
2094
+ # ======================================================== Math extensions (vector) ========================================================
2095
+
2096
+ def dotProduct(a: list[float], b: list[float]) -> float:
2097
+ """Compute dot product of two vectors."""
2098
+ if len(a) != len(b):
2099
+ raise ValueError("Vectors must have same length")
2100
+ return sum(x * y for x, y in zip(a, b))
2101
+
2102
+
2103
+ def crossProduct(a: list[float], b: list[float]) -> list[float]:
2104
+ """Compute cross product of two 3D vectors."""
2105
+ if len(a) != 3 or len(b) != 3:
2106
+ raise ValueError("Cross product requires 3D vectors")
2107
+ return [
2108
+ a[1] * b[2] - a[2] * b[1],
2109
+ a[2] * b[0] - a[0] * b[2],
2110
+ a[0] * b[1] - a[1] * b[0],
2111
+ ]
2112
+
2113
+
2114
+ def normalize(v: list[float]) -> list[float]:
2115
+ """Normalize a vector to unit length."""
2116
+ mag = sum(x ** 2 for x in v) ** 0.5
2117
+ if mag == 0:
2118
+ raise ValueError("Cannot normalize zero vector")
2119
+ return [x / mag for x in v]
2120
+
2121
+
2122
+ # ======================================================== Local storage ========================================================
2123
+
2124
+ import os
2125
+
2126
+
2127
+ class Storage:
2128
+ """Simple local key-value storage with file persistence.
2129
+
2130
+ Supports subpaths (relative to base) and absolute paths.
2131
+ """
2132
+
2133
+ def __init__(self, base_path: str = ".eapy_storage"):
2134
+ self.base_path = os.path.abspath(base_path)
2135
+ os.makedirs(self.base_path, exist_ok=True)
2136
+
2137
+ def _resolve_path(self, key: str) -> str:
2138
+ """Resolve key to file path. Absolute paths used as-is."""
2139
+ if os.path.isabs(key):
2140
+ path = key
2141
+ else:
2142
+ path = os.path.join(self.base_path, key)
2143
+ os.makedirs(os.path.dirname(path), exist_ok=True)
2144
+ return path
2145
+
2146
+ def set(self, key: str, value):
2147
+ """Store a value."""
2148
+ path = self._resolve_path(key)
2149
+ with open(path, "w", encoding="utf-8") as f:
2150
+ json.dump(value, f, ensure_ascii=False)
2151
+
2152
+ def get(self, key: str, default=None):
2153
+ """Retrieve a value."""
2154
+ path = self._resolve_path(key)
2155
+ if not os.path.exists(path):
2156
+ return default
2157
+ with open(path, "r", encoding="utf-8") as f:
2158
+ return json.load(f)
2159
+
2160
+ def delete(self, key: str):
2161
+ """Delete a stored value."""
2162
+ path = self._resolve_path(key)
2163
+ if os.path.exists(path):
2164
+ os.remove(path)
2165
+
2166
+ def exists(self, key: str) -> bool:
2167
+ """Check if key exists."""
2168
+ return os.path.exists(self._resolve_path(key))
2169
+
2170
+ def keys(self, prefix: str = "") -> list[str]:
2171
+ """List all keys under optional prefix."""
2172
+ result = []
2173
+ search_path = self._resolve_path(prefix) if prefix else self.base_path
2174
+ if not os.path.isdir(search_path):
2175
+ return result
2176
+ for root, dirs, files in os.walk(search_path):
2177
+ for f in files:
2178
+ full = os.path.join(root, f)
2179
+ rel = os.path.relpath(full, self.base_path)
2180
+ result.append(rel)
2181
+ return result
2182
+
2183
+ def clear(self):
2184
+ """Remove all stored data."""
2185
+ import shutil
2186
+ if os.path.exists(self.base_path):
2187
+ shutil.rmtree(self.base_path)
2188
+ os.makedirs(self.base_path, exist_ok=True)
2189
+
2190
+ def __repr__(self):
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))
@@ -31,8 +31,16 @@ from eapy.core import (
31
31
  base64Encode, base64Decode, urlEncode, urlDecode,
32
32
  factorial, matrixInverse, interpolate,
33
33
  extractEmails, extractURLs, replaceMultiple,
34
+ lruCache, timeit,
35
+ tqdm,
36
+ isNumber, isString, isList, isDict, isCallable,
37
+ dotProduct, crossProduct, normalize,
38
+ Storage,
39
+ Color, colorText, printColor,
34
40
  )
35
41
  from math import pi
42
+ import os
43
+ import shutil
36
44
 
37
45
 
38
46
  # ======================================================== Timer ========================================================
@@ -604,3 +612,141 @@ class TestRegex:
604
612
  def test_replaceMultiple(self):
605
613
  result = replaceMultiple("hello world", {"hello": "hi", "world": "earth"})
606
614
  assert result == "hi earth"
615
+
616
+
617
+ # ======================================================== Cache / Performance ========================================================
618
+
619
+ class TestCache:
620
+ def test_lruCache(self):
621
+ call_count = [0]
622
+
623
+ @lruCache(maxsize=2)
624
+ def add(a, b):
625
+ call_count[0] += 1
626
+ return a + b
627
+
628
+ assert add(1, 2) == 3
629
+ assert add(1, 2) == 3 # cached
630
+ assert call_count[0] == 1
631
+
632
+ def test_timeit(self, capsys):
633
+ @timeit
634
+ def noop():
635
+ pass
636
+ noop()
637
+ captured = capsys.readouterr()
638
+ assert "executed in" in captured.out
639
+
640
+
641
+ # ======================================================== Progress iterator ========================================================
642
+
643
+ class TestTqdm:
644
+ def test_iteration(self):
645
+ result = list(tqdm([1, 2, 3], total=3))
646
+ assert result == [1, 2, 3]
647
+
648
+
649
+ # ======================================================== Type checks ========================================================
650
+
651
+ class TestTypeChecks:
652
+ def test_isNumber(self):
653
+ assert isNumber(42) is True
654
+ assert isNumber(3.14) is True
655
+ assert isNumber("42") is False
656
+ assert isNumber(True) is False
657
+
658
+ def test_isString(self):
659
+ assert isString("hello") is True
660
+ assert isString(42) is False
661
+
662
+ def test_isList(self):
663
+ assert isList([1, 2]) is True
664
+ assert isList((1, 2)) is False
665
+
666
+ def test_isDict(self):
667
+ assert isDict({"a": 1}) is True
668
+ assert isDict([1, 2]) is False
669
+
670
+ def test_isCallable(self):
671
+ assert isCallable(lambda: None) is True
672
+ assert isCallable(42) is False
673
+
674
+
675
+ # ======================================================== Math extensions (vector) ========================================================
676
+
677
+ class TestVectorMath:
678
+ def test_dotProduct(self):
679
+ assert dotProduct([1, 2, 3], [4, 5, 6]) == 32
680
+
681
+ def test_crossProduct(self):
682
+ assert crossProduct([1, 0, 0], [0, 1, 0]) == [0, 0, 1]
683
+
684
+ def test_normalize(self):
685
+ result = normalize([3, 4])
686
+ assert abs(result[0] - 0.6) < 1e-10
687
+ assert abs(result[1] - 0.8) < 1e-10
688
+
689
+
690
+ # ======================================================== Local storage ========================================================
691
+
692
+ class TestStorage:
693
+ def setup_method(self):
694
+ self.test_dir = ".test_storage"
695
+ self.store = Storage(self.test_dir)
696
+
697
+ def teardown_method(self):
698
+ if os.path.exists(self.test_dir):
699
+ shutil.rmtree(self.test_dir)
700
+
701
+ def test_set_get(self):
702
+ self.store.set("key1", {"name": "test", "value": 42})
703
+ result = self.store.get("key1")
704
+ assert result == {"name": "test", "value": 42}
705
+
706
+ def test_get_default(self):
707
+ assert self.store.get("nonexistent", "default") == "default"
708
+
709
+ def test_delete(self):
710
+ self.store.set("key2", "value")
711
+ self.store.delete("key2")
712
+ assert self.store.exists("key2") is False
713
+
714
+ def test_subpath(self):
715
+ self.store.set("sub/dir/key", [1, 2, 3])
716
+ assert self.store.get("sub/dir/key") == [1, 2, 3]
717
+
718
+ def test_keys(self):
719
+ self.store.set("a", 1)
720
+ self.store.set("b", 2)
721
+ keys = self.store.keys()
722
+ assert "a" in keys
723
+ assert "b" in keys
724
+
725
+ def test_clear(self):
726
+ self.store.set("x", 1)
727
+ self.store.clear()
728
+ assert self.store.exists("x") is False
729
+
730
+
731
+ # ======================================================== Colored text ========================================================
732
+
733
+ class TestColor:
734
+ def test_colorText(self):
735
+ result = colorText("hello", Color.RED)
736
+ assert Color.RED in result
737
+ assert Color.RESET in result
738
+ assert "hello" in result
739
+
740
+ def test_colorText_with_style(self):
741
+ result = colorText("bold", Color.GREEN, Color.BOLD)
742
+ assert Color.BOLD in result
743
+ assert Color.GREEN in result
744
+
745
+ def test_colorText_with_bg(self):
746
+ result = colorText("bg", bg=Color.BG_BLUE)
747
+ assert Color.BG_BLUE in result
748
+
749
+ def test_printColor(self, capsys):
750
+ printColor("test", Color.YELLOW)
751
+ captured = capsys.readouterr()
752
+ assert "test" in captured.out
File without changes
File without changes
File without changes
File without changes
File without changes