ygo 1.0.2__py3-none-any.whl → 1.2.12__py3-none-any.whl

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.
ygo/utils.py ADDED
@@ -0,0 +1,230 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ---------------------------------------------
4
+ Created on 2025/5/26 23:31
5
+ @author: ZhangYundi
6
+ @email: yundi.xxii@outlook.com
7
+ ---------------------------------------------
8
+ """
9
+
10
+ import inspect
11
+ import os
12
+ from functools import wraps
13
+ from pathlib import Path
14
+ import warnings
15
+ from typing import Any
16
+
17
+ from .delay import delay
18
+
19
+ def deprecated(use_instead: str = None):
20
+ """
21
+ 标记方法为弃用
22
+
23
+ Parameters
24
+ ----------
25
+ use_instead: str
26
+ 推荐替代使用的方法或者类名称
27
+ Returns
28
+ -------
29
+
30
+ """
31
+
32
+ def decorator(func):
33
+ """
34
+ 装饰器
35
+ Parameters
36
+ ----------
37
+ func: callable
38
+ 被装饰的函数
39
+ Returns
40
+ -------
41
+
42
+ """
43
+ @wraps(func)
44
+ def wrapper(*args, **kwargs):
45
+ msg = f"`{func.__name__}` is deprecated. "
46
+ if use_instead:
47
+ msg += f"Please use `{use_instead}` instead."
48
+ warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
49
+ return func(*args, **kwargs)
50
+ return wrapper
51
+ return decorator
52
+
53
+ def fn_params(func: callable):
54
+ """
55
+ 获取fn的参数
56
+ Parameters
57
+ ----------
58
+ func: callable
59
+ 需要获取参数的callable对象
60
+ Returns
61
+ -------
62
+ list[tuple]
63
+
64
+ """
65
+ stored = delay(func)().stored_kwargs.items()
66
+ return sorted(stored)
67
+
68
+
69
+ def fn_signature_params(func: callable):
70
+ """获取fn所有定义的参数"""
71
+ return sorted(list(inspect.signature(func).parameters.keys()))
72
+
73
+
74
+ def fn_path(fn: callable) -> str:
75
+ """
76
+ 获取func所在的模块层级结构
77
+ Parameters
78
+ ----------
79
+ fn: callable
80
+ 需要获取结构的callable对象
81
+ Returns
82
+ -------
83
+ str
84
+ 用 `.` 连接各级层级
85
+ """
86
+ module = fn.__module__
87
+ # 检查模块是否有 __file__ 属性
88
+ if module.startswith('__main__'):
89
+ if hasattr(module, '__file__'):
90
+ module = module.__file__
91
+ else:
92
+ # 如果在交互式环境中,返回 None 或者一个默认值
93
+ module = "<interactive environment>"
94
+ if module.endswith('.py'):
95
+ module = module.split('.py')[0].split(str(Path(__file__).parent.parent.absolute()))[-1]
96
+ module = '.'.join(module.strip(os.sep).split(os.sep))
97
+ return module
98
+
99
+
100
+ def fn_code(fn: callable) -> str:
101
+ """
102
+ 返回fn具体的定义代码
103
+
104
+ Parameters
105
+ ----------
106
+ fn: callable
107
+ 需要获取具体定义代码的callable对象
108
+
109
+ Returns
110
+ -------
111
+ str
112
+ 以字符串封装定义代码
113
+
114
+ Examples
115
+ --------
116
+
117
+ >>> def test_fn(a, b=2):
118
+ >>> return a+b
119
+ >>> print(fn_code())
120
+ def test_fn(a, b=2):
121
+ return a+b
122
+ """
123
+ return inspect.getsource(fn)
124
+
125
+
126
+ def fn_info(fn: callable) -> str:
127
+ """获取函数的fn_mod, params, code"""
128
+ # mod = fn_path(fn)
129
+ params = fn_params(fn)
130
+ code = fn_code(fn)
131
+ all_define_params = sorted(list(inspect.signature(fn).parameters.keys()))
132
+
133
+ default_params = {k: v for k, v in params}
134
+ params_infos = list()
135
+ for p in all_define_params:
136
+ if p in default_params:
137
+ params_infos.append(f'{p}={default_params[p]}')
138
+ else:
139
+ params_infos.append(p)
140
+ params_infos = ', '.join(params_infos)
141
+
142
+ s = f"""
143
+ =============================================================
144
+ {fn.__name__}({params_infos})
145
+ =============================================================
146
+ {code}
147
+ """
148
+ return s
149
+
150
+ @deprecated("lazy_import")
151
+ def fn_from_str(s):
152
+ """
153
+ 字符串导入对应fn
154
+ s: a.b.c.func
155
+ Parameters
156
+ ----------
157
+ s: str
158
+ 模块的路径,分隔符 `.`
159
+ """
160
+ import importlib
161
+ *m_path, func = s.split(".")
162
+ m_path = ".".join(m_path)
163
+ mod = importlib.import_module(m_path)
164
+ _callable = getattr(mod, func)
165
+ return _callable
166
+
167
+ @deprecated("lazy_import")
168
+ def module_from_str(s):
169
+ """字符串导入模块"""
170
+ import importlib
171
+ m_path = ".".join(s.split('.'))
172
+ mod = importlib.import_module(m_path)
173
+ return mod
174
+
175
+ def locate(path: str) -> Any:
176
+ """
177
+ Notes
178
+ -----
179
+ Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
180
+
181
+ Locate an object by name or dotted path, importing as necessary.
182
+ This is similar to the pydoc function `locate`, except that it checks for
183
+ the module from the given path from back to front.
184
+ """
185
+ if path == "":
186
+ raise ImportError("Empty path")
187
+ from importlib import import_module
188
+ from types import ModuleType
189
+
190
+ parts = [part for part in path.split(".")]
191
+ for part in parts:
192
+ if not len(part):
193
+ raise ValueError(
194
+ f"Error loading '{path}': invalid dotstring."
195
+ + "\nRelative imports are not supported."
196
+ )
197
+ assert len(parts) > 0
198
+ part0 = parts[0]
199
+ try:
200
+ obj = import_module(part0)
201
+ except Exception as exc_import:
202
+ raise ImportError(
203
+ f"Error loading '{path}':\n{repr(exc_import)}"
204
+ + f"\nAre you sure that module '{part0}' is installed?"
205
+ ) from exc_import
206
+ for m in range(1, len(parts)):
207
+ part = parts[m]
208
+ try:
209
+ obj = getattr(obj, part)
210
+ except AttributeError as exc_attr:
211
+ parent_dotpath = ".".join(parts[:m])
212
+ if isinstance(obj, ModuleType):
213
+ mod = ".".join(parts[: m + 1])
214
+ try:
215
+ obj = import_module(mod)
216
+ continue
217
+ except ModuleNotFoundError as exc_import:
218
+ raise ImportError(
219
+ f"Error loading '{path}':\n{repr(exc_import)}"
220
+ + f"\nAre you sure that '{part}' is importable from module '{parent_dotpath}'?"
221
+ ) from exc_import
222
+ except Exception as exc_import:
223
+ raise ImportError(
224
+ f"Error loading '{path}':\n{repr(exc_import)}"
225
+ ) from exc_import
226
+ raise ImportError(
227
+ f"Error loading '{path}':\n{repr(exc_attr)}"
228
+ + f"\nAre you sure that '{part}' is an attribute of '{parent_dotpath}'?"
229
+ ) from exc_attr
230
+ return obj
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: ygo
3
+ Version: 1.2.12
4
+ Summary: 一个轻量级 Python 工具包,支持 并发执行(带进度条)、延迟调用、链式绑定参数、函数信息获取、模块/函数动态加载。
5
+ Project-URL: homepage, https://github.com/link-yundi/ygo
6
+ Project-URL: repository, https://github.com/link-yundi/ygo
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: joblib>=1.5.0
11
+ Requires-Dist: logair>=1.0.6
12
+ Requires-Dist: psutil>=7.2.1
13
+ Requires-Dist: tqdm>=4.67.1
14
+ Dynamic: license-file
15
+
16
+ # ygo
17
+ 一个轻量级 Python 工具包,底层基于 joblib 和 tqdm 、loguru 实现,支持
18
+ - 并发执行(带进度条)
19
+ - 延迟调用
20
+ - 链式绑定参数
21
+ - 函数信息获取
22
+ - 模块/函数动态加载...
23
+ - 并结合 ylog 提供日志记录能力
24
+
25
+ ### 安装
26
+ ```shell
27
+ pip install -U ygo
28
+ ```
29
+
30
+ ### 🧰 功能概览
31
+
32
+ | 模块 | 功能 |
33
+ | :----- | :----------------------------------------------------------- |
34
+ | `ygo` | 支持并发执行(带进度条)、延迟调用、函数信息获取以及模块/函数动态加载等功能 |
35
+ | `ylog` | 日志模块,提供统一的日志输出接口 |
36
+
37
+ ### 示例
38
+
39
+ ```
40
+ ├── a
41
+ │   ├── __init__.py
42
+ │   └── b
43
+ │   ├── __init__.py
44
+ │   └── c.py
45
+ └── test.py
46
+
47
+ c.py 中定义了目标函数
48
+ def test_fn(a, b=2):
49
+ return a+b
50
+ ```
51
+
52
+ #### 场景1: 并发执行
53
+
54
+ ```python
55
+ import ygo
56
+ import ylog
57
+ from a.b.c import test_fn
58
+
59
+ with ygo.pool(n_jobs=5, show_progress=True) as go:
60
+ for i in range(10):
61
+ go.submit(test_fn)(a=i, b=2*i)
62
+ for res in go.do():
63
+ ylog.info(res)
64
+ ```
65
+
66
+ #### ✅ `ygo.pool` 支持的参数
67
+
68
+ | 参数名 | 类型 | 描述 |
69
+ | ------------- | ---- | ------------------------------------------------------------ |
70
+ | n_jobs | int | 并行任务数(<=1 表示串行) |
71
+ | show_progress | bool | 是否显示进度条 |
72
+ | backend | str | 执行后端(默认 'threading',可选 'multiprocessing' 或 'loky') |
73
+
74
+ #### 场景2: 延迟调用
75
+
76
+ ```
77
+ >>> fn = delay(test_fn)(a=1, b=2)
78
+ >>> fn()
79
+ 3
80
+ >>> # 逐步传递参数
81
+ >>> fn1 = delay(lambda a, b, c: a+b+c)(a=1)
82
+ >>> fn2 = delay(fn1)(b=2)
83
+ >>> fn2(c=3)
84
+ 6
85
+ >>> # 参数更改
86
+ >>> fn1 = delay(lambda a, b, c: a+b+c)(a=1, b=2)
87
+ >>> fn2 = delay(fn1)(c=3, b=5)
88
+ >>> fn2()
89
+ 9
90
+ ```
91
+
92
+ #### 场景3: 获取目标函数信息
93
+
94
+ ```
95
+ >>> ygo.fn_info(test_fn)
96
+ =============================================================
97
+ a.b.c.test_fn(a, b=2)
98
+ =============================================================
99
+ def test_fn(a, b=2):
100
+ return a+b
101
+ ```
102
+
103
+ #### 🔍 其他函数信息工具
104
+
105
+ | 方法名 | 描述 |
106
+ | ------------------------- | ---------------------------------------- |
107
+ | `fn_params(fn)` | 获取函数实参 |
108
+ | `fn_signature_params(fn)` | 获取函数定义的所有参数名 |
109
+ | `fn_code(fn)` | 获取函数源码字符串 |
110
+ | `fn_path(fn)` | 获取函数所属模块路径 |
111
+ | `fn_from_str(s)` | 根据字符串导入函数(如 "a.b.c.test_fn") |
112
+ | `module_from_str(s)` | 根据字符串导入模块 |
113
+
114
+ #### 场景4: 通过字符串解析函数并执行
115
+
116
+ ```
117
+ >>> ygo.fn_from_str("a.b.c.test_fn")(a=1, b=5)
118
+ 6
119
+ ```
@@ -0,0 +1,11 @@
1
+ ygo/__init__.py,sha256=vP3Vs9LarNsn2TV8QlpuRP94WWK7m3q-gIWw6vLRED0,537
2
+ ygo/delay.py,sha256=66xtPXqyD630FL7LWL5qJKAIZvyGDwZyM4qPfk8Czlg,2206
3
+ ygo/exceptions.py,sha256=0OYDYt_9KKo8mF2XBG5QkCMr3-ASp69VDSPOEwlIsrI,660
4
+ ygo/lazy.py,sha256=NRE38vWKfA19fTRIbgCypdoxXhJulDtHMd7jw6CzhUc,1303
5
+ ygo/pool.py,sha256=hKvUttBhK2gU4WHocvg-2bjS9uVJ9jITbRb1tL2JYY4,10699
6
+ ygo/utils.py,sha256=45ZM7FUraDA69jwf1arbBuuS-a0wMVf9sT7ma5bGd4E,6224
7
+ ygo-1.2.12.dist-info/licenses/LICENSE,sha256=6AKUWQ1xe-jwPSFv_H6FMQLNNWb7AYqzuEUTwlP2S8M,1067
8
+ ygo-1.2.12.dist-info/METADATA,sha256=awbNtAfd34cw1Rjyc2WHerxgw4U8kSoozKgtUNNCQWw,3612
9
+ ygo-1.2.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ ygo-1.2.12.dist-info/top_level.txt,sha256=4E07GOD3KS5--r4rP13upXfTEgk3JGJpQmMkdkJ4t74,4
11
+ ygo-1.2.12.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1 @@
1
+ ygo
ycat/__init__.py DELETED
@@ -1,34 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- ---------------------------------------------
4
- Created on 2025/5/14 18:29
5
- @author: ZhangYundi
6
- @email: yundi.xxii@outlook.com
7
- ---------------------------------------------
8
- """
9
-
10
- from .client import (
11
- HOME,
12
- CATDB,
13
- get_settings,
14
- sql,
15
- put,
16
- create_engine_ck,
17
- create_engine_mysql,
18
- read_mysql,
19
- read_ck,
20
- tb_path,
21
- )
22
-
23
- __all__ = [
24
- "HOME",
25
- "CATDB",
26
- "get_settings",
27
- "sql",
28
- "put",
29
- "create_engine_ck",
30
- "create_engine_mysql",
31
- "read_mysql",
32
- "read_ck",
33
- "tb_path",
34
- ]
ycat/client.py DELETED
@@ -1,142 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- ---------------------------------------------
4
- Created on 2024/7/1 09:44
5
- @author: ZhangYundi
6
- @email: yundi.xxii@outlook.com
7
- ---------------------------------------------
8
- """
9
- import os
10
- import re
11
- from functools import partial
12
- from typing import Optional
13
-
14
- import polars as pl
15
- from dynaconf import Dynaconf
16
- from sqlalchemy import create_engine
17
-
18
- import ylog
19
- from .parse import extract_table_names_from_sql
20
- from .yck import connect, query_polars
21
-
22
- # 配置文件在 “~/.catdb/setting.toml”
23
- USERHOME = os.path.expanduser('~') # 用户家目录
24
- NAME = "catdb"
25
- CONFIG_PATH = os.path.join(USERHOME, f".{NAME}", "settings.toml")
26
- if not os.path.exists(CONFIG_PATH):
27
- try:
28
- os.makedirs(os.path.dirname(CONFIG_PATH))
29
- except FileExistsError as e:
30
- ...
31
- except Exception as e:
32
- ylog.error(f"配置文件生成失败: {e}")
33
- catdb_path = os.path.join(USERHOME, NAME)
34
- template_content = f"""[paths]
35
- {NAME}="{catdb_path}" # 本地数据库,默认家目录
36
-
37
- ## 数据库配置:
38
- [database]
39
- [database.ck]
40
- # urls=["<host1>:<port1>", "<host2>:<port2>",]
41
- # user="xxx"
42
- # password="xxxxxx"
43
- [database.jy]
44
- # url="<host>:<port>"
45
- # user="xxxx"
46
- # password="xxxxxx"
47
-
48
- ## 视情况自由增加其他配置
49
- """
50
- with open(CONFIG_PATH, "w") as f:
51
- f.write(template_content)
52
- ylog.info(f"生成配置文件: {CONFIG_PATH}")
53
-
54
-
55
- def get_settings():
56
- try:
57
- return Dynaconf(settings_files=[CONFIG_PATH]).as_dict()
58
- except:
59
- return {}
60
-
61
-
62
- HOME = USERHOME
63
- CATDB = os.path.join(HOME, NAME)
64
- # 读取配置文件覆盖
65
- SETTINGS = get_settings()
66
- if SETTINGS is not None:
67
- CATDB = SETTINGS["PATHS"][NAME]
68
- if not CATDB.endswith(NAME):
69
- CATDB = os.path.join(CATDB, NAME)
70
-
71
-
72
- # ======================== 本地数据库 catdb ========================
73
- def tb_path(tb_name: str) -> str:
74
- """
75
- 返回指定表名 完整的本地路径
76
- Parameters
77
- ----------
78
- tb_name: str
79
- 表名,路径写法: a/b/c
80
- Returns
81
- -------
82
- full_abs_path: str
83
- 完整的本地绝对路径 $HOME/catdb/a/b/c
84
- """
85
- return os.path.join(CATDB, tb_name)
86
-
87
-
88
- def put(df: pl.DataFrame, tb_name: str, partitions: Optional[list[str]] = None, abs_path: bool = False):
89
- if not abs_path:
90
- tbpath = tb_path(tb_name)
91
- else:
92
- tbpath = tb_name
93
- if not os.path.exists(tbpath):
94
- try:
95
- os.makedirs(tbpath)
96
- except FileExistsError as e:
97
- pass
98
- if partitions is not None:
99
- for field in partitions:
100
- assert field in df.columns, f'dataframe must have Field `{field}`'
101
- df.write_parquet(tbpath, partition_by=partitions)
102
-
103
-
104
- def sql(query: str, abs_path: bool = False):
105
- tbs = extract_table_names_from_sql(query)
106
- convertor = dict()
107
- for tb in tbs:
108
- if not abs_path:
109
- db_path = tb_path(tb)
110
- else:
111
- db_path = tb
112
- format_tb = f"read_parquet('{db_path}/**/*.parquet')"
113
- convertor[tb] = format_tb
114
- pattern = re.compile("|".join(re.escape(k) for k in convertor.keys()))
115
- new_query = pattern.sub(lambda m: convertor[m.group(0)], query)
116
- return pl.sql(new_query).collect()
117
-
118
-
119
- def create_engine_ck(urls: list[str], user: str, password: str):
120
- return partial(connect, urls, user, password)
121
-
122
-
123
- def read_ck(sql, eng) -> pl.DataFrame:
124
- with eng() as conn:
125
- return query_polars(sql, conn)
126
-
127
-
128
- def create_engine_mysql(url, user, password, database):
129
- """
130
- :param url: <host>:<port>
131
- :param user:
132
- :param password:
133
- :param database:
134
- :return:
135
- """
136
- engine = create_engine(f"mysql+pymysql://{user}:{password}@{url}/{database}")
137
- return engine
138
-
139
-
140
- def read_mysql(sql, eng) -> pl.DataFrame:
141
- with eng.connect() as conn:
142
- return pl.read_database(sql, conn)