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.
- {eapytool-0.2.0 → eapytool-0.3.1}/PKG-INFO +41 -1
- {eapytool-0.2.0 → eapytool-0.3.1}/README.md +40 -0
- {eapytool-0.2.0 → eapytool-0.3.1}/pyproject.toml +1 -1
- {eapytool-0.2.0 → eapytool-0.3.1}/src/eapy/__init__.py +22 -1
- {eapytool-0.2.0 → eapytool-0.3.1}/src/eapy/core.py +515 -1
- {eapytool-0.2.0 → eapytool-0.3.1}/tests/test_core.py +233 -0
- {eapytool-0.2.0 → eapytool-0.3.1}/.gitignore +0 -0
- {eapytool-0.2.0 → eapytool-0.3.1}/LICENSE +0 -0
- {eapytool-0.2.0 → eapytool-0.3.1}/src/eapy/py.typed +0 -0
- {eapytool-0.2.0 → eapytool-0.3.1}/tests/__init__.py +0 -0
- {eapytool-0.2.0 → eapytool-0.3.1}/tests/conftest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eapyTool
|
|
3
|
-
Version: 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
|
|
@@ -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
|
+
__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
|