eapyTool 0.2.0__tar.gz → 0.3.1__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.2.0
3
+ Version: 0.3.1
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
@@ -63,6 +63,9 @@ A Python utility library that extends the standard library with math, algebra, d
63
63
  - `Stack` — LIFO stack (push/pop/peek)
64
64
  - `Queue` — FIFO queue (enqueue/dequeue)
65
65
  - `Trie` — prefix tree (insert/search/words_with_prefix)
66
+ - `LinkedList` — singly linked list (append/prepend/remove/find)
67
+ - `CircularBuffer` — fixed-size circular buffer
68
+ - `PriorityQueue` — min-heap priority queue
66
69
 
67
70
  ### Base Conversion
68
71
  - `baseConvert(n, from_base, to_base)` — arbitrary base conversion (2-36)
@@ -107,6 +110,43 @@ A Python utility library that extends the standard library with math, algebra, d
107
110
  ### Random Utilities
108
111
  - `randomChoice`, `shuffle`, `randomString`, `randomInt`, `randomFloat` — enhanced random helpers
109
112
 
113
+ ### File I/O
114
+ - `readJson`, `writeJson` — JSON file helpers
115
+ - `readCsv`, `writeCsv` — CSV file helpers
116
+
117
+ ### Encoding
118
+ - `base64Encode`, `base64Decode` — Base64 encoding
119
+ - `urlEncode`, `urlDecode` — URL encoding
120
+
121
+ ### Math Extensions
122
+ - `factorial` — factorial
123
+ - `matrixInverse` — matrix inverse (Gauss-Jordan)
124
+ - `interpolate` — linear interpolation
125
+
126
+ ### Regex Utilities
127
+ - `extractEmails`, `extractURLs` — extract from text
128
+ - `replaceMultiple` — multi-pattern replacement
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
+
110
150
  ## Installation
111
151
 
112
152
  ```bash
@@ -40,6 +40,9 @@ A Python utility library that extends the standard library with math, algebra, d
40
40
  - `Stack` — LIFO stack (push/pop/peek)
41
41
  - `Queue` — FIFO queue (enqueue/dequeue)
42
42
  - `Trie` — prefix tree (insert/search/words_with_prefix)
43
+ - `LinkedList` — singly linked list (append/prepend/remove/find)
44
+ - `CircularBuffer` — fixed-size circular buffer
45
+ - `PriorityQueue` — min-heap priority queue
43
46
 
44
47
  ### Base Conversion
45
48
  - `baseConvert(n, from_base, to_base)` — arbitrary base conversion (2-36)
@@ -84,6 +87,43 @@ A Python utility library that extends the standard library with math, algebra, d
84
87
  ### Random Utilities
85
88
  - `randomChoice`, `shuffle`, `randomString`, `randomInt`, `randomFloat` — enhanced random helpers
86
89
 
90
+ ### File I/O
91
+ - `readJson`, `writeJson` — JSON file helpers
92
+ - `readCsv`, `writeCsv` — CSV file helpers
93
+
94
+ ### Encoding
95
+ - `base64Encode`, `base64Decode` — Base64 encoding
96
+ - `urlEncode`, `urlDecode` — URL encoding
97
+
98
+ ### Math Extensions
99
+ - `factorial` — factorial
100
+ - `matrixInverse` — matrix inverse (Gauss-Jordan)
101
+ - `interpolate` — linear interpolation
102
+
103
+ ### Regex Utilities
104
+ - `extractEmails`, `extractURLs` — extract from text
105
+ - `replaceMultiple` — multi-pattern replacement
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
+
87
127
  ## Installation
88
128
 
89
129
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "eapyTool"
7
- version = "0.2.0"
7
+ version = "0.3.1"
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.2.0"
3
+ __version__ = "0.3.1"
4
4
 
5
5
  from eapy.core import (
6
6
  # Timer
@@ -51,6 +51,7 @@ from eapy.core import (
51
51
  pipe, compose, curry,
52
52
  # Data structures
53
53
  Stack, Queue, Trie,
54
+ LinkedList, CircularBuffer, PriorityQueue,
54
55
  # Base conversion
55
56
  baseConvert, toBinary, toOctal, toHex,
56
57
  # Color
@@ -69,4 +70,24 @@ from eapy.core import (
69
70
  md5, sha256, hashFile,
70
71
  # Random
71
72
  randomChoice, shuffle, randomString, randomInt, randomFloat,
73
+ # File I/O
74
+ readJson, writeJson, readCsv, writeCsv,
75
+ # Encoding
76
+ base64Encode, base64Decode, urlEncode, urlDecode,
77
+ # Math extensions
78
+ factorial, matrixInverse, interpolate,
79
+ # Regex utilities
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,
72
93
  )
@@ -1674,4 +1674,518 @@ def randomInt(min_val: int, max_val: int) -> int:
1674
1674
 
1675
1675
  def randomFloat(min_val: float = 0.0, max_val: float = 1.0) -> float:
1676
1676
  """Return a random float in [min_val, max_val)."""
1677
- return _random.uniform(min_val, max_val)
1677
+ return _random.uniform(min_val, max_val)
1678
+
1679
+
1680
+ # ======================================================== Data structures (extended) ========================================================
1681
+
1682
+ class LinkedListNode:
1683
+ def __init__(self, value, next_node=None):
1684
+ self.value = value
1685
+ self.next = next_node
1686
+
1687
+
1688
+ class LinkedList:
1689
+ """Singly linked list."""
1690
+
1691
+ def __init__(self):
1692
+ self.head = None
1693
+ self._size = 0
1694
+
1695
+ def append(self, value):
1696
+ """Add value to the end."""
1697
+ new_node = LinkedListNode(value)
1698
+ if self.head is None:
1699
+ self.head = new_node
1700
+ else:
1701
+ node = self.head
1702
+ while node.next:
1703
+ node = node.next
1704
+ node.next = new_node
1705
+ self._size += 1
1706
+
1707
+ def prepend(self, value):
1708
+ """Add value to the beginning."""
1709
+ self.head = LinkedListNode(value, self.head)
1710
+ self._size += 1
1711
+
1712
+ def remove(self, value):
1713
+ """Remove first occurrence of value."""
1714
+ if self.head is None:
1715
+ raise ValueError(f"{value} not found")
1716
+ if self.head.value == value:
1717
+ self.head = self.head.next
1718
+ self._size -= 1
1719
+ return
1720
+ node = self.head
1721
+ while node.next:
1722
+ if node.next.value == value:
1723
+ node.next = node.next.next
1724
+ self._size -= 1
1725
+ return
1726
+ node = node.next
1727
+ raise ValueError(f"{value} not found")
1728
+
1729
+ def find(self, value):
1730
+ """Return True if value exists."""
1731
+ node = self.head
1732
+ while node:
1733
+ if node.value == value:
1734
+ return True
1735
+ node = node.next
1736
+ return False
1737
+
1738
+ def to_list(self) -> list:
1739
+ """Convert to Python list."""
1740
+ result = []
1741
+ node = self.head
1742
+ while node:
1743
+ result.append(node.value)
1744
+ node = node.next
1745
+ return result
1746
+
1747
+ def __len__(self):
1748
+ return self._size
1749
+
1750
+ def __repr__(self):
1751
+ return f"LinkedList({self.to_list()})"
1752
+
1753
+
1754
+ class CircularBuffer:
1755
+ """Fixed-size circular buffer."""
1756
+
1757
+ def __init__(self, capacity: int):
1758
+ self.capacity = capacity
1759
+ self._data = [None] * capacity
1760
+ self._start = 0
1761
+ self._end = 0
1762
+ self._size = 0
1763
+
1764
+ def push(self, value):
1765
+ """Add value, overwrites oldest if full."""
1766
+ self._data[self._end] = value
1767
+ self._end = (self._end + 1) % self.capacity
1768
+ if self._size == self.capacity:
1769
+ self._start = (self._start + 1) % self.capacity
1770
+ else:
1771
+ self._size += 1
1772
+
1773
+ def pop(self):
1774
+ """Remove and return the oldest value."""
1775
+ if self._size == 0:
1776
+ raise IndexError("Buffer is empty")
1777
+ value = self._data[self._start]
1778
+ self._start = (self._start + 1) % self.capacity
1779
+ self._size -= 1
1780
+ return value
1781
+
1782
+ def peek(self):
1783
+ """Return oldest value without removing."""
1784
+ if self._size == 0:
1785
+ raise IndexError("Buffer is empty")
1786
+ return self._data[self._start]
1787
+
1788
+ def __len__(self):
1789
+ return self._size
1790
+
1791
+ def __repr__(self):
1792
+ items = []
1793
+ idx = self._start
1794
+ for _ in range(self._size):
1795
+ items.append(self._data[idx])
1796
+ idx = (idx + 1) % self.capacity
1797
+ return f"CircularBuffer({items})"
1798
+
1799
+
1800
+ class PriorityQueue:
1801
+ """Min-heap priority queue."""
1802
+
1803
+ def __init__(self):
1804
+ self._heap = []
1805
+
1806
+ def push(self, item, priority: float = 0):
1807
+ """Add item with given priority (lower = higher priority)."""
1808
+ self._heap.append((priority, item))
1809
+ self._heap.sort(key=lambda x: x[0])
1810
+
1811
+ def pop(self):
1812
+ """Remove and return item with lowest priority value."""
1813
+ if not self._heap:
1814
+ raise IndexError("Queue is empty")
1815
+ return self._heap.pop(0)[1]
1816
+
1817
+ def peek(self):
1818
+ """Return item with lowest priority value."""
1819
+ if not self._heap:
1820
+ raise IndexError("Queue is empty")
1821
+ return self._heap[0][1]
1822
+
1823
+ def is_empty(self) -> bool:
1824
+ return len(self._heap) == 0
1825
+
1826
+ def __len__(self):
1827
+ return len(self._heap)
1828
+
1829
+ def __repr__(self):
1830
+ return f"PriorityQueue({[(p, i) for p, i in self._heap]})"
1831
+
1832
+
1833
+ # ======================================================== File I/O ========================================================
1834
+
1835
+ import json
1836
+ import csv
1837
+
1838
+
1839
+ def readJson(path: str, encoding: str = "utf-8"):
1840
+ """Read and parse a JSON file."""
1841
+ with open(path, "r", encoding=encoding) as f:
1842
+ return json.load(f)
1843
+
1844
+
1845
+ def writeJson(path: str, data, indent: int = 2, encoding: str = "utf-8"):
1846
+ """Write data to a JSON file."""
1847
+ with open(path, "w", encoding=encoding) as f:
1848
+ json.dump(data, f, indent=indent, ensure_ascii=False)
1849
+
1850
+
1851
+ def readCsv(path: str, delimiter: str = ",", encoding: str = "utf-8") -> list[list[str]]:
1852
+ """Read a CSV file and return list of rows."""
1853
+ with open(path, "r", encoding=encoding, newline="") as f:
1854
+ return list(csv.reader(f, delimiter=delimiter))
1855
+
1856
+
1857
+ def writeCsv(path: str, data: list[list], delimiter: str = ",", encoding: str = "utf-8"):
1858
+ """Write list of rows to a CSV file."""
1859
+ with open(path, "w", encoding=encoding, newline="") as f:
1860
+ writer = csv.writer(f, delimiter=delimiter)
1861
+ writer.writerows(data)
1862
+
1863
+
1864
+ # ======================================================== Encoding ========================================================
1865
+
1866
+ import base64
1867
+ import urllib.parse
1868
+
1869
+
1870
+ def base64Encode(s: str) -> str:
1871
+ """Encode string to Base64."""
1872
+ return base64.b64encode(s.encode()).decode()
1873
+
1874
+
1875
+ def base64Decode(s: str) -> str:
1876
+ """Decode Base64 string."""
1877
+ return base64.b64decode(s.encode()).decode()
1878
+
1879
+
1880
+ def urlEncode(s: str) -> str:
1881
+ """URL-encode a string."""
1882
+ return urllib.parse.quote(s)
1883
+
1884
+
1885
+ def urlDecode(s: str) -> str:
1886
+ """URL-decode a string."""
1887
+ return urllib.parse.unquote(s)
1888
+
1889
+
1890
+ # ======================================================== Math extensions ========================================================
1891
+
1892
+ def factorial(n: int) -> int:
1893
+ """Return n! (n factorial)."""
1894
+ if n < 0:
1895
+ raise ValueError("Factorial not defined for negative numbers")
1896
+ if n <= 1:
1897
+ return 1
1898
+ result = 1
1899
+ for i in range(2, n + 1):
1900
+ result *= i
1901
+ return result
1902
+
1903
+
1904
+ def matrixInverse(m: Matrix) -> Matrix:
1905
+ """Return the inverse of a square matrix using Gauss-Jordan elimination."""
1906
+ n = len(m.data)
1907
+ if any(len(row) != n for row in m.data):
1908
+ raise ValueError("Matrix must be square")
1909
+ # Augment with identity
1910
+ aug = [m.data[i][:] + [1.0 if i == j else 0.0 for j in range(n)] for i in range(n)]
1911
+ for col in range(n):
1912
+ # Find pivot
1913
+ max_row = col
1914
+ for row in range(col + 1, n):
1915
+ if abs(aug[row][col]) > abs(aug[max_row][col]):
1916
+ max_row = row
1917
+ aug[col], aug[max_row] = aug[max_row], aug[col]
1918
+ if abs(aug[col][col]) < 1e-12:
1919
+ raise ValueError("Matrix is singular")
1920
+ pivot = aug[col][col]
1921
+ for j in range(2 * n):
1922
+ aug[col][j] /= pivot
1923
+ for row in range(n):
1924
+ if row != col:
1925
+ factor = aug[row][col]
1926
+ for j in range(2 * n):
1927
+ aug[row][j] -= factor * aug[col][j]
1928
+ return Matrix([row[n:] for row in aug])
1929
+
1930
+
1931
+ def interpolate(x: float, points: list[tuple[float, float]], method: str = "linear") -> float:
1932
+ """Interpolate at x given data points. Methods: 'linear'."""
1933
+ xs = [p[0] for p in points]
1934
+ ys = [p[1] for p in points]
1935
+ if method == "linear":
1936
+ if x <= xs[0]:
1937
+ return ys[0]
1938
+ if x >= xs[-1]:
1939
+ return ys[-1]
1940
+ for i in range(len(xs) - 1):
1941
+ if xs[i] <= x <= xs[i + 1]:
1942
+ t = (x - xs[i]) / (xs[i + 1] - xs[i])
1943
+ return ys[i] + t * (ys[i + 1] - ys[i])
1944
+ raise ValueError(f"Unknown method: {method}")
1945
+
1946
+
1947
+ # ======================================================== Regex utilities ========================================================
1948
+
1949
+
1950
+ def extractEmails(text: str) -> list[str]:
1951
+ """Extract all email addresses from text."""
1952
+ return re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text)
1953
+
1954
+
1955
+ def extractURLs(text: str) -> list[str]:
1956
+ """Extract all URLs from text."""
1957
+ return re.findall(r'https?://[^\s<>"]+', text)
1958
+
1959
+
1960
+ def replaceMultiple(text: str, replacements: dict[str, str]) -> str:
1961
+ """Replace multiple patterns in text. Keys are patterns, values are replacements."""
1962
+ for pattern, replacement in replacements.items():
1963
+ text = text.replace(pattern, replacement)
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}')"
@@ -17,6 +17,7 @@ from eapy.core import (
17
17
  toCamelCase, toSnakeCase, toPascalCase, levenshtein,
18
18
  pipe, compose, curry,
19
19
  Stack, Queue, Trie,
20
+ LinkedList, CircularBuffer, PriorityQueue,
20
21
  baseConvert, toBinary, toOctal, toHex,
21
22
  rgbToHsl, hslToRgb, rgbToHex, hexToRgb,
22
23
  bisect, newtonMethod, numericalIntegral,
@@ -27,8 +28,18 @@ from eapy.core import (
27
28
  ProgressBar,
28
29
  md5, sha256,
29
30
  randomChoice, shuffle, randomString, randomInt, randomFloat,
31
+ base64Encode, base64Decode, urlEncode, urlDecode,
32
+ factorial, matrixInverse, interpolate,
33
+ extractEmails, extractURLs, replaceMultiple,
34
+ lruCache, timeit,
35
+ tqdm,
36
+ isNumber, isString, isList, isDict, isCallable,
37
+ dotProduct, crossProduct, normalize,
38
+ Storage,
30
39
  )
31
40
  from math import pi
41
+ import os
42
+ import shutil
32
43
 
33
44
 
34
45
  # ======================================================== Timer ========================================================
@@ -492,3 +503,225 @@ class TestRandom:
492
503
 
493
504
  def test_randomFloat(self):
494
505
  assert 0.0 <= randomFloat(0.0, 1.0) < 1.0
506
+
507
+
508
+ # ======================================================== Data structures (extended) ========================================================
509
+
510
+ class TestLinkedList:
511
+ def test_append_and_find(self):
512
+ ll = LinkedList()
513
+ ll.append(1)
514
+ ll.append(2)
515
+ ll.append(3)
516
+ assert ll.find(2) is True
517
+ assert ll.find(5) is False
518
+ assert ll.to_list() == [1, 2, 3]
519
+
520
+ def test_remove(self):
521
+ ll = LinkedList()
522
+ ll.append(1)
523
+ ll.append(2)
524
+ ll.remove(2)
525
+ assert ll.to_list() == [1]
526
+
527
+ def test_prepend(self):
528
+ ll = LinkedList()
529
+ ll.prepend(1)
530
+ ll.prepend(0)
531
+ assert ll.to_list() == [0, 1]
532
+
533
+
534
+ class TestCircularBuffer:
535
+ def test_push_pop(self):
536
+ cb = CircularBuffer(3)
537
+ cb.push(1)
538
+ cb.push(2)
539
+ cb.push(3)
540
+ assert cb.pop() == 1
541
+ assert cb.pop() == 2
542
+
543
+ def test_overwrite(self):
544
+ cb = CircularBuffer(2)
545
+ cb.push(1)
546
+ cb.push(2)
547
+ cb.push(3) # overwrites 1
548
+ assert cb.pop() == 2
549
+ assert cb.pop() == 3
550
+
551
+
552
+ class TestPriorityQueue:
553
+ def test_order(self):
554
+ pq = PriorityQueue()
555
+ pq.push("low", priority=3)
556
+ pq.push("high", priority=1)
557
+ pq.push("mid", priority=2)
558
+ assert pq.pop() == "high"
559
+ assert pq.pop() == "mid"
560
+ assert pq.pop() == "low"
561
+
562
+
563
+ # ======================================================== Encoding ========================================================
564
+
565
+ class TestEncoding:
566
+ def test_base64(self):
567
+ encoded = base64Encode("hello")
568
+ assert base64Decode(encoded) == "hello"
569
+
570
+ def test_url(self):
571
+ encoded = urlEncode("hello world&foo=bar")
572
+ assert " " not in encoded
573
+ assert urlDecode(encoded) == "hello world&foo=bar"
574
+
575
+
576
+ # ======================================================== Math extensions ========================================================
577
+
578
+ class TestMathExtensions:
579
+ def test_factorial(self):
580
+ assert factorial(5) == 120
581
+ assert factorial(0) == 1
582
+
583
+ def test_matrixInverse(self):
584
+ m = Matrix([[1, 2], [3, 4]])
585
+ inv = matrixInverse(m)
586
+ # m * inv should be identity
587
+ result = m * inv
588
+ assert abs(result.data[0][0] - 1) < 1e-10
589
+ assert abs(result.data[1][1] - 1) < 1e-10
590
+
591
+ def test_interpolate(self):
592
+ points = [(0, 0), (10, 100)]
593
+ assert interpolate(5, points) == 50.0
594
+
595
+
596
+ # ======================================================== Regex utilities ========================================================
597
+
598
+ class TestRegex:
599
+ def test_extractEmails(self):
600
+ text = "Contact us at test@example.com or admin@site.org"
601
+ emails = extractEmails(text)
602
+ assert "test@example.com" in emails
603
+ assert "admin@site.org" in emails
604
+
605
+ def test_extractURLs(self):
606
+ text = "Visit https://example.com or http://test.org"
607
+ urls = extractURLs(text)
608
+ assert "https://example.com" in urls
609
+ assert "http://test.org" in urls
610
+
611
+ def test_replaceMultiple(self):
612
+ result = replaceMultiple("hello world", {"hello": "hi", "world": "earth"})
613
+ assert result == "hi earth"
614
+
615
+
616
+ # ======================================================== Cache / Performance ========================================================
617
+
618
+ class TestCache:
619
+ def test_lruCache(self):
620
+ call_count = [0]
621
+
622
+ @lruCache(maxsize=2)
623
+ def add(a, b):
624
+ call_count[0] += 1
625
+ return a + b
626
+
627
+ assert add(1, 2) == 3
628
+ assert add(1, 2) == 3 # cached
629
+ assert call_count[0] == 1
630
+
631
+ def test_timeit(self, capsys):
632
+ @timeit
633
+ def noop():
634
+ pass
635
+ noop()
636
+ captured = capsys.readouterr()
637
+ assert "executed in" in captured.out
638
+
639
+
640
+ # ======================================================== Progress iterator ========================================================
641
+
642
+ class TestTqdm:
643
+ def test_iteration(self):
644
+ result = list(tqdm([1, 2, 3], total=3))
645
+ assert result == [1, 2, 3]
646
+
647
+
648
+ # ======================================================== Type checks ========================================================
649
+
650
+ class TestTypeChecks:
651
+ def test_isNumber(self):
652
+ assert isNumber(42) is True
653
+ assert isNumber(3.14) is True
654
+ assert isNumber("42") is False
655
+ assert isNumber(True) is False
656
+
657
+ def test_isString(self):
658
+ assert isString("hello") is True
659
+ assert isString(42) is False
660
+
661
+ def test_isList(self):
662
+ assert isList([1, 2]) is True
663
+ assert isList((1, 2)) is False
664
+
665
+ def test_isDict(self):
666
+ assert isDict({"a": 1}) is True
667
+ assert isDict([1, 2]) is False
668
+
669
+ def test_isCallable(self):
670
+ assert isCallable(lambda: None) is True
671
+ assert isCallable(42) is False
672
+
673
+
674
+ # ======================================================== Math extensions (vector) ========================================================
675
+
676
+ class TestVectorMath:
677
+ def test_dotProduct(self):
678
+ assert dotProduct([1, 2, 3], [4, 5, 6]) == 32
679
+
680
+ def test_crossProduct(self):
681
+ assert crossProduct([1, 0, 0], [0, 1, 0]) == [0, 0, 1]
682
+
683
+ def test_normalize(self):
684
+ result = normalize([3, 4])
685
+ assert abs(result[0] - 0.6) < 1e-10
686
+ assert abs(result[1] - 0.8) < 1e-10
687
+
688
+
689
+ # ======================================================== Local storage ========================================================
690
+
691
+ class TestStorage:
692
+ def setup_method(self):
693
+ self.test_dir = ".test_storage"
694
+ self.store = Storage(self.test_dir)
695
+
696
+ def teardown_method(self):
697
+ if os.path.exists(self.test_dir):
698
+ shutil.rmtree(self.test_dir)
699
+
700
+ def test_set_get(self):
701
+ self.store.set("key1", {"name": "test", "value": 42})
702
+ result = self.store.get("key1")
703
+ assert result == {"name": "test", "value": 42}
704
+
705
+ def test_get_default(self):
706
+ assert self.store.get("nonexistent", "default") == "default"
707
+
708
+ def test_delete(self):
709
+ self.store.set("key2", "value")
710
+ self.store.delete("key2")
711
+ assert self.store.exists("key2") is False
712
+
713
+ def test_subpath(self):
714
+ self.store.set("sub/dir/key", [1, 2, 3])
715
+ assert self.store.get("sub/dir/key") == [1, 2, 3]
716
+
717
+ def test_keys(self):
718
+ self.store.set("a", 1)
719
+ self.store.set("b", 2)
720
+ keys = self.store.keys()
721
+ assert "a" in keys
722
+ assert "b" in keys
723
+
724
+ def test_clear(self):
725
+ self.store.set("x", 1)
726
+ self.store.clear()
727
+ assert self.store.exists("x") is False
File without changes
File without changes
File without changes
File without changes
File without changes