pyrubix 1.0.0__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.
pyrubix-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrubix
3
+ Version: 1.0.0
4
+ Summary: Powerful lightweight Python utility library
5
+ Project-URL: Homepage, https://xnovear.dpdns.org
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+
9
+ # ULIB
10
+
11
+ Lightweight Python utility library.
12
+
13
+ Version: 3.0.0
14
+
15
+ ## Features
16
+
17
+ - Math
18
+ - Text
19
+ - JSON
20
+ - System
21
+ - Time
22
+ - Cache
23
+ - Retry
24
+ - Timer
25
+ - File utilities
26
+ - Subprocess
27
+ - Hash
28
+ - UUID
29
+ - Random
30
+ - Collections
31
+ - Network
32
+ - Logging
33
+
34
+ ## Install
35
+
36
+ python -m pip install -e .
37
+
38
+ ## Example
39
+
40
+ import ulib
41
+
42
+ print(ulib.add(10, 20))
43
+ print(ulib.sha256("hello"))
44
+ print(ulib.uuid())
45
+
46
+ result = ulib.run("ls -la")
47
+ print(result.stdout)
@@ -0,0 +1,39 @@
1
+ # ULIB
2
+
3
+ Lightweight Python utility library.
4
+
5
+ Version: 3.0.0
6
+
7
+ ## Features
8
+
9
+ - Math
10
+ - Text
11
+ - JSON
12
+ - System
13
+ - Time
14
+ - Cache
15
+ - Retry
16
+ - Timer
17
+ - File utilities
18
+ - Subprocess
19
+ - Hash
20
+ - UUID
21
+ - Random
22
+ - Collections
23
+ - Network
24
+ - Logging
25
+
26
+ ## Install
27
+
28
+ python -m pip install -e .
29
+
30
+ ## Example
31
+
32
+ import ulib
33
+
34
+ print(ulib.add(10, 20))
35
+ print(ulib.sha256("hello"))
36
+ print(ulib.uuid())
37
+
38
+ result = ulib.run("ls -la")
39
+ print(result.stdout)
@@ -0,0 +1,13 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyrubix"
7
+ version = "1.0.0"
8
+ description = "Powerful lightweight Python utility library"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+
12
+ [project.urls]
13
+ Homepage = "https://xnovear.dpdns.org"
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrubix
3
+ Version: 1.0.0
4
+ Summary: Powerful lightweight Python utility library
5
+ Project-URL: Homepage, https://xnovear.dpdns.org
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+
9
+ # ULIB
10
+
11
+ Lightweight Python utility library.
12
+
13
+ Version: 3.0.0
14
+
15
+ ## Features
16
+
17
+ - Math
18
+ - Text
19
+ - JSON
20
+ - System
21
+ - Time
22
+ - Cache
23
+ - Retry
24
+ - Timer
25
+ - File utilities
26
+ - Subprocess
27
+ - Hash
28
+ - UUID
29
+ - Random
30
+ - Collections
31
+ - Network
32
+ - Logging
33
+
34
+ ## Install
35
+
36
+ python -m pip install -e .
37
+
38
+ ## Example
39
+
40
+ import ulib
41
+
42
+ print(ulib.add(10, 20))
43
+ print(ulib.sha256("hello"))
44
+ print(ulib.uuid())
45
+
46
+ result = ulib.run("ls -la")
47
+ print(result.stdout)
@@ -0,0 +1,22 @@
1
+ README.md
2
+ pyproject.toml
3
+ pyrubix.egg-info/PKG-INFO
4
+ pyrubix.egg-info/SOURCES.txt
5
+ pyrubix.egg-info/dependency_links.txt
6
+ pyrubix.egg-info/top_level.txt
7
+ ulib/__init__.py
8
+ ulib/cache.py
9
+ ulib/collections.py
10
+ ulib/crypto.py
11
+ ulib/decorators.py
12
+ ulib/fileutil.py
13
+ ulib/identity.py
14
+ ulib/jsonx.py
15
+ ulib/loggingx.py
16
+ ulib/math.py
17
+ ulib/network.py
18
+ ulib/process.py
19
+ ulib/randomutil.py
20
+ ulib/system.py
21
+ ulib/text.py
22
+ ulib/timeutil.py
@@ -0,0 +1 @@
1
+ ulib
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,221 @@
1
+ from .math import (
2
+ add,
3
+ subtract,
4
+ multiply,
5
+ divide,
6
+ power,
7
+ average,
8
+ median,
9
+ clamp,
10
+ percentage,
11
+ lerp,
12
+ map_range,
13
+ factorial,
14
+ gcd,
15
+ lcm,
16
+ )
17
+
18
+ from .text import (
19
+ reverse,
20
+ is_palindrome,
21
+ word_count,
22
+ char_count,
23
+ slugify,
24
+ truncate,
25
+ camel_case,
26
+ snake_case,
27
+ clean,
28
+ remove_spaces,
29
+ is_email,
30
+ is_url,
31
+ is_number,
32
+ )
33
+
34
+ from .jsonx import (
35
+ dumps,
36
+ loads,
37
+ save,
38
+ load,
39
+ )
40
+
41
+ from .system import (
42
+ platform,
43
+ python_version,
44
+ cpu_count,
45
+ memory_usage,
46
+ env,
47
+ set_env,
48
+ which,
49
+ )
50
+
51
+ from .timeutil import (
52
+ now,
53
+ timestamp,
54
+ elapsed,
55
+ sleep,
56
+ )
57
+
58
+ from .cache import (
59
+ memoize,
60
+ TTLCache,
61
+ )
62
+
63
+ from .decorators import (
64
+ timer,
65
+ retry,
66
+ )
67
+
68
+ from .fileutil import (
69
+ read_text,
70
+ write_text,
71
+ read_bytes,
72
+ write_bytes,
73
+ exists,
74
+ is_file,
75
+ is_dir,
76
+ file_size,
77
+ copy,
78
+ move,
79
+ remove,
80
+ mkdir,
81
+ listdir,
82
+ )
83
+
84
+ from .process import (
85
+ run,
86
+ )
87
+
88
+ from .crypto import (
89
+ md5,
90
+ sha256,
91
+ sha512,
92
+ file_md5,
93
+ file_sha256,
94
+ file_sha512,
95
+ )
96
+
97
+ from .identity import (
98
+ uuid,
99
+ short_uuid,
100
+ )
101
+
102
+ from .randomutil import (
103
+ random_int,
104
+ random_float,
105
+ random_choice,
106
+ random_string,
107
+ )
108
+
109
+ from .collections import (
110
+ chunk,
111
+ flatten,
112
+ unique,
113
+ )
114
+
115
+ from .network import (
116
+ is_online,
117
+ ping,
118
+ get_public_ip,
119
+ )
120
+
121
+ from .loggingx import (
122
+ log,
123
+ )
124
+
125
+ __version__ = "3.0.0"
126
+
127
+ __all__ = [
128
+ "add",
129
+ "subtract",
130
+ "multiply",
131
+ "divide",
132
+ "power",
133
+ "average",
134
+ "median",
135
+ "clamp",
136
+ "percentage",
137
+ "lerp",
138
+ "map_range",
139
+ "factorial",
140
+ "gcd",
141
+ "lcm",
142
+
143
+ "reverse",
144
+ "is_palindrome",
145
+ "word_count",
146
+ "char_count",
147
+ "slugify",
148
+ "truncate",
149
+ "camel_case",
150
+ "snake_case",
151
+ "clean",
152
+ "remove_spaces",
153
+ "is_email",
154
+ "is_url",
155
+ "is_number",
156
+
157
+ "dumps",
158
+ "loads",
159
+ "save",
160
+ "load",
161
+
162
+ "platform",
163
+ "python_version",
164
+ "cpu_count",
165
+ "memory_usage",
166
+ "env",
167
+ "set_env",
168
+ "which",
169
+
170
+ "now",
171
+ "timestamp",
172
+ "elapsed",
173
+ "sleep",
174
+
175
+ "memoize",
176
+ "TTLCache",
177
+
178
+ "timer",
179
+ "retry",
180
+
181
+ "read_text",
182
+ "write_text",
183
+ "read_bytes",
184
+ "write_bytes",
185
+ "exists",
186
+ "is_file",
187
+ "is_dir",
188
+ "file_size",
189
+ "copy",
190
+ "move",
191
+ "remove",
192
+ "mkdir",
193
+ "listdir",
194
+
195
+ "run",
196
+
197
+ "md5",
198
+ "sha256",
199
+ "sha512",
200
+ "file_md5",
201
+ "file_sha256",
202
+ "file_sha512",
203
+
204
+ "uuid",
205
+ "short_uuid",
206
+
207
+ "random_int",
208
+ "random_float",
209
+ "random_choice",
210
+ "random_string",
211
+
212
+ "chunk",
213
+ "flatten",
214
+ "unique",
215
+
216
+ "is_online",
217
+ "ping",
218
+ "get_public_ip",
219
+
220
+ "log",
221
+ ]
@@ -0,0 +1,73 @@
1
+ import time
2
+ from functools import wraps
3
+
4
+
5
+ def memoize(func):
6
+ cache = {}
7
+
8
+ @wraps(func)
9
+ def wrapper(*args, **kwargs):
10
+ key = (
11
+ args,
12
+ tuple(sorted(kwargs.items())),
13
+ )
14
+
15
+ if key not in cache:
16
+ cache[key] = func(*args, **kwargs)
17
+
18
+ return cache[key]
19
+
20
+ wrapper.cache = cache
21
+
22
+ return wrapper
23
+
24
+
25
+ class TTLCache:
26
+ def __init__(self, ttl=60):
27
+ if ttl < 0:
28
+ raise ValueError("ttl cannot be negative")
29
+
30
+ self.ttl = ttl
31
+ self._data = {}
32
+
33
+ def set(self, key, value):
34
+ self._data[key] = (
35
+ value,
36
+ time.monotonic() + self.ttl,
37
+ )
38
+
39
+ def get(self, key, default=None):
40
+ item = self._data.get(key)
41
+
42
+ if item is None:
43
+ return default
44
+
45
+ value, expires = item
46
+
47
+ if time.monotonic() >= expires:
48
+ self._data.pop(key, None)
49
+ return default
50
+
51
+ return value
52
+
53
+ def delete(self, key):
54
+ self._data.pop(key, None)
55
+
56
+ def clear(self):
57
+ self._data.clear()
58
+
59
+ def __len__(self):
60
+ self._cleanup()
61
+ return len(self._data)
62
+
63
+ def _cleanup(self):
64
+ current = time.monotonic()
65
+
66
+ expired = [
67
+ key
68
+ for key, (_, expires) in self._data.items()
69
+ if current >= expires
70
+ ]
71
+
72
+ for key in expired:
73
+ self._data.pop(key, None)
@@ -0,0 +1,41 @@
1
+ def chunk(values, size):
2
+ if size <= 0:
3
+ raise ValueError("size must be greater than zero")
4
+
5
+ values = list(values)
6
+
7
+ return [
8
+ values[i:i + size]
9
+ for i in range(0, len(values), size)
10
+ ]
11
+
12
+
13
+ def flatten(values):
14
+ result = []
15
+
16
+ for item in values:
17
+ if isinstance(item, (list, tuple)):
18
+ result.extend(flatten(item))
19
+ else:
20
+ result.append(item)
21
+
22
+ return result
23
+
24
+
25
+ def unique(values):
26
+ result = []
27
+ seen = set()
28
+
29
+ for item in values:
30
+ try:
31
+ key = item
32
+
33
+ if key not in seen:
34
+ seen.add(key)
35
+ result.append(item)
36
+
37
+ except TypeError:
38
+ if item not in result:
39
+ result.append(item)
40
+
41
+ return result
@@ -0,0 +1,47 @@
1
+ import hashlib
2
+
3
+
4
+ def _hash_text(text, algorithm):
5
+ return hashlib.new(
6
+ algorithm,
7
+ str(text).encode("utf-8"),
8
+ ).hexdigest()
9
+
10
+
11
+ def md5(text):
12
+ return _hash_text(text, "md5")
13
+
14
+
15
+ def sha256(text):
16
+ return _hash_text(text, "sha256")
17
+
18
+
19
+ def sha512(text):
20
+ return _hash_text(text, "sha512")
21
+
22
+
23
+ def _hash_file(path, algorithm, chunk_size=1024 * 1024):
24
+ h = hashlib.new(algorithm)
25
+
26
+ with open(path, "rb") as f:
27
+ while True:
28
+ chunk = f.read(chunk_size)
29
+
30
+ if not chunk:
31
+ break
32
+
33
+ h.update(chunk)
34
+
35
+ return h.hexdigest()
36
+
37
+
38
+ def file_md5(path):
39
+ return _hash_file(path, "md5")
40
+
41
+
42
+ def file_sha256(path):
43
+ return _hash_file(path, "sha256")
44
+
45
+
46
+ def file_sha512(path):
47
+ return _hash_file(path, "sha512")
@@ -0,0 +1,50 @@
1
+ import time
2
+ from functools import wraps
3
+
4
+
5
+ def timer(func):
6
+ @wraps(func)
7
+ def wrapper(*args, **kwargs):
8
+ start = time.perf_counter()
9
+
10
+ try:
11
+ return func(*args, **kwargs)
12
+ finally:
13
+ elapsed = time.perf_counter() - start
14
+ print(f"[ULIB] {func.__name__}: {elapsed:.6f}s")
15
+
16
+ return wrapper
17
+
18
+
19
+ def retry(
20
+ attempts=3,
21
+ delay=0,
22
+ exceptions=(Exception,),
23
+ ):
24
+ if attempts < 1:
25
+ raise ValueError("attempts must be >= 1")
26
+
27
+ if delay < 0:
28
+ raise ValueError("delay cannot be negative")
29
+
30
+ def decorator(func):
31
+ @wraps(func)
32
+ def wrapper(*args, **kwargs):
33
+ last_error = None
34
+
35
+ for attempt in range(attempts):
36
+ try:
37
+ return func(*args, **kwargs)
38
+
39
+ except exceptions as error:
40
+ last_error = error
41
+
42
+ if attempt < attempts - 1:
43
+ if delay:
44
+ time.sleep(delay)
45
+
46
+ raise last_error
47
+
48
+ return wrapper
49
+
50
+ return decorator
@@ -0,0 +1,61 @@
1
+ import os
2
+ import shutil
3
+
4
+
5
+ def read_text(path, encoding="utf-8"):
6
+ with open(path, "r", encoding=encoding) as f:
7
+ return f.read()
8
+
9
+
10
+ def write_text(path, text, encoding="utf-8"):
11
+ with open(path, "w", encoding=encoding) as f:
12
+ f.write(str(text))
13
+
14
+
15
+ def read_bytes(path):
16
+ with open(path, "rb") as f:
17
+ return f.read()
18
+
19
+
20
+ def write_bytes(path, data):
21
+ with open(path, "wb") as f:
22
+ f.write(data)
23
+
24
+
25
+ def exists(path):
26
+ return os.path.exists(path)
27
+
28
+
29
+ def is_file(path):
30
+ return os.path.isfile(path)
31
+
32
+
33
+ def is_dir(path):
34
+ return os.path.isdir(path)
35
+
36
+
37
+ def file_size(path):
38
+ return os.path.getsize(path)
39
+
40
+
41
+ def copy(src, dst):
42
+ return shutil.copy2(src, dst)
43
+
44
+
45
+ def move(src, dst):
46
+ return shutil.move(src, dst)
47
+
48
+
49
+ def remove(path):
50
+ if os.path.isdir(path) and not os.path.islink(path):
51
+ shutil.rmtree(path)
52
+ else:
53
+ os.remove(path)
54
+
55
+
56
+ def mkdir(path, parents=True, exist_ok=True):
57
+ os.makedirs(path, exist_ok=exist_ok) if parents else os.mkdir(path)
58
+
59
+
60
+ def listdir(path="."):
61
+ return os.listdir(path)
@@ -0,0 +1,9 @@
1
+ import uuid as _uuid
2
+
3
+
4
+ def uuid():
5
+ return str(_uuid.uuid4())
6
+
7
+
8
+ def short_uuid():
9
+ return _uuid.uuid4().hex[:12]
@@ -0,0 +1,28 @@
1
+ import json
2
+
3
+
4
+ def dumps(obj, indent=2, ensure_ascii=False):
5
+ return json.dumps(
6
+ obj,
7
+ indent=indent,
8
+ ensure_ascii=ensure_ascii,
9
+ )
10
+
11
+
12
+ def loads(text):
13
+ return json.loads(text)
14
+
15
+
16
+ def save(obj, path, indent=2):
17
+ with open(path, "w", encoding="utf-8") as f:
18
+ json.dump(
19
+ obj,
20
+ f,
21
+ indent=indent,
22
+ ensure_ascii=False,
23
+ )
24
+
25
+
26
+ def load(path):
27
+ with open(path, "r", encoding="utf-8") as f:
28
+ return json.load(f)
@@ -0,0 +1,9 @@
1
+ import datetime
2
+
3
+
4
+ def log(message, level="INFO"):
5
+ now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
6
+
7
+ print(
8
+ f"[{now}] [{str(level).upper()}] {message}"
9
+ )
@@ -0,0 +1,83 @@
1
+ import math
2
+
3
+
4
+ def add(a, b):
5
+ return a + b
6
+
7
+
8
+ def subtract(a, b):
9
+ return a - b
10
+
11
+
12
+ def multiply(a, b):
13
+ return a * b
14
+
15
+
16
+ def divide(a, b):
17
+ if b == 0:
18
+ raise ZeroDivisionError("cannot divide by zero")
19
+ return a / b
20
+
21
+
22
+ def power(a, b):
23
+ return a ** b
24
+
25
+
26
+ def average(values):
27
+ values = list(values)
28
+
29
+ if not values:
30
+ raise ValueError("values cannot be empty")
31
+
32
+ return sum(values) / len(values)
33
+
34
+
35
+ def median(values):
36
+ values = sorted(values)
37
+
38
+ if not values:
39
+ raise ValueError("values cannot be empty")
40
+
41
+ return math.median(values)
42
+
43
+
44
+ def clamp(value, minimum, maximum):
45
+ if minimum > maximum:
46
+ raise ValueError("minimum cannot be greater than maximum")
47
+
48
+ return max(minimum, min(value, maximum))
49
+
50
+
51
+ def percentage(value, total):
52
+ if total == 0:
53
+ raise ZeroDivisionError("total cannot be zero")
54
+
55
+ return value / total * 100
56
+
57
+
58
+ def lerp(a, b, t):
59
+ return a + (b - a) * t
60
+
61
+
62
+ def map_range(value, in_min, in_max, out_min, out_max):
63
+ if in_min == in_max:
64
+ raise ValueError("input range cannot be zero")
65
+
66
+ return (
67
+ (value - in_min)
68
+ * (out_max - out_min)
69
+ / (in_max - in_min)
70
+ + out_min
71
+ )
72
+
73
+
74
+ def factorial(n):
75
+ return math.factorial(n)
76
+
77
+
78
+ def gcd(a, b):
79
+ return math.gcd(a, b)
80
+
81
+
82
+ def lcm(a, b):
83
+ return math.lcm(a, b)
@@ -0,0 +1,41 @@
1
+ import socket
2
+ import urllib.request
3
+
4
+
5
+ def is_online(timeout=3):
6
+ try:
7
+ with socket.create_connection(
8
+ ("1.1.1.1", 53),
9
+ timeout=timeout,
10
+ ):
11
+ return True
12
+
13
+ except OSError:
14
+ return False
15
+
16
+
17
+ def ping(host, port=80, timeout=3):
18
+ try:
19
+ with socket.create_connection(
20
+ (host, port),
21
+ timeout=timeout,
22
+ ):
23
+ return True
24
+
25
+ except OSError:
26
+ return False
27
+
28
+
29
+ def get_public_ip(timeout=5):
30
+ request = urllib.request.Request(
31
+ "https://api.ipify.org",
32
+ headers={
33
+ "User-Agent": "ulib/3.0",
34
+ },
35
+ )
36
+
37
+ with urllib.request.urlopen(
38
+ request,
39
+ timeout=timeout,
40
+ ) as response:
41
+ return response.read().decode("utf-8").strip()
@@ -0,0 +1,22 @@
1
+ import subprocess
2
+
3
+
4
+ def run(
5
+ command,
6
+ *,
7
+ shell=True,
8
+ check=False,
9
+ capture=True,
10
+ text=True,
11
+ timeout=None,
12
+ ):
13
+ result = subprocess.run(
14
+ command,
15
+ shell=shell,
16
+ check=check,
17
+ capture_output=capture,
18
+ text=text,
19
+ timeout=timeout,
20
+ )
21
+
22
+ return result
@@ -0,0 +1,31 @@
1
+ import random
2
+ import string
3
+
4
+
5
+ def random_int(minimum, maximum):
6
+ return random.randint(minimum, maximum)
7
+
8
+
9
+ def random_float(minimum=0.0, maximum=1.0):
10
+ return random.uniform(minimum, maximum)
11
+
12
+
13
+ def random_choice(values):
14
+ values = list(values)
15
+
16
+ if not values:
17
+ raise ValueError("values cannot be empty")
18
+
19
+ return random.choice(values)
20
+
21
+
22
+ def random_string(length):
23
+ if length < 0:
24
+ raise ValueError("length cannot be negative")
25
+
26
+ alphabet = string.ascii_letters + string.digits
27
+
28
+ return "".join(
29
+ random.choice(alphabet)
30
+ for _ in range(length)
31
+ )
@@ -0,0 +1,47 @@
1
+ import os
2
+ import platform as _platform
3
+ import shutil
4
+ import sys
5
+
6
+ try:
7
+ import resource
8
+ except ImportError:
9
+ resource = None
10
+
11
+
12
+ def platform():
13
+ return _platform.platform()
14
+
15
+
16
+ def python_version():
17
+ return _platform.python_version()
18
+
19
+
20
+ def cpu_count():
21
+ return os.cpu_count() or 1
22
+
23
+
24
+ def memory_usage():
25
+ if resource is None:
26
+ return None
27
+
28
+ usage = resource.getrusage(resource.RUSAGE_SELF)
29
+
30
+ value = usage.ru_maxrss
31
+
32
+ if sys.platform == "darwin":
33
+ return value
34
+
35
+ return value * 1024
36
+
37
+
38
+ def env(name, default=None):
39
+ return os.environ.get(name, default)
40
+
41
+
42
+ def set_env(name, value):
43
+ os.environ[str(name)] = str(value)
44
+
45
+
46
+ def which(command):
47
+ return shutil.which(command)
@@ -0,0 +1,87 @@
1
+ import re
2
+
3
+
4
+ def reverse(text):
5
+ return text[::-1]
6
+
7
+
8
+ def is_palindrome(text):
9
+ cleaned = "".join(c.lower() for c in text if c.isalnum())
10
+ return cleaned == cleaned[::-1]
11
+
12
+
13
+ def word_count(text):
14
+ return len(text.split())
15
+
16
+
17
+ def char_count(text, include_spaces=False):
18
+ if include_spaces:
19
+ return len(text)
20
+
21
+ return len("".join(text.split()))
22
+
23
+
24
+ def slugify(text):
25
+ text = text.strip().lower()
26
+ text = re.sub(r"[^0-9a-z가-힣]+", "-", text)
27
+ return text.strip("-")
28
+
29
+
30
+ def truncate(text, length, suffix="..."):
31
+ if length < 0:
32
+ raise ValueError("length cannot be negative")
33
+
34
+ if len(text) <= length:
35
+ return text
36
+
37
+ if length <= len(suffix):
38
+ return suffix[:length]
39
+
40
+ return text[:length - len(suffix)] + suffix
41
+
42
+
43
+ def camel_case(text):
44
+ parts = re.split(r"[\s_\-]+", text.strip())
45
+
46
+ if not parts:
47
+ return ""
48
+
49
+ return parts[0].lower() + "".join(
50
+ part[:1].upper() + part[1:].lower()
51
+ for part in parts[1:]
52
+ if part
53
+ )
54
+
55
+
56
+ def snake_case(text):
57
+ text = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", text)
58
+ text = re.sub(r"[\s\-]+", "_", text)
59
+ text = re.sub(r"[^0-9a-zA-Z가-힣_]", "", text)
60
+
61
+ return text.lower().strip("_")
62
+
63
+
64
+ def clean(text):
65
+ return " ".join(text.split())
66
+
67
+
68
+ def remove_spaces(text):
69
+ return "".join(text.split())
70
+
71
+
72
+ def is_email(text):
73
+ pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$"
74
+ return bool(re.match(pattern, text))
75
+
76
+
77
+ def is_url(text):
78
+ pattern = r"^https?://[^\s]+$"
79
+ return bool(re.match(pattern, text))
80
+
81
+
82
+ def is_number(text):
83
+ try:
84
+ float(text)
85
+ return True
86
+ except (TypeError, ValueError):
87
+ return False
@@ -0,0 +1,18 @@
1
+ import time
2
+ from datetime import datetime, timezone
3
+
4
+
5
+ def now():
6
+ return datetime.now(timezone.utc)
7
+
8
+
9
+ def timestamp():
10
+ return time.time()
11
+
12
+
13
+ def elapsed(start):
14
+ return time.perf_counter() - start
15
+
16
+
17
+ def sleep(seconds):
18
+ time.sleep(seconds)