python-basekit 0.0.11__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.
- basekit/__init__.py +0 -0
- basekit/ai/clients/anthropic.py +192 -0
- basekit/ai/clients/dashscope.py +133 -0
- basekit/ai/clients/gemini.py +277 -0
- basekit/ai/clients/openai.py +384 -0
- basekit/ai/schema.py +160 -0
- basekit/ai/utils.py +63 -0
- basekit/cache/clients/sqlite.py +90 -0
- basekit/cache/schema.py +53 -0
- basekit/cache/utils.py +113 -0
- basekit/database.py +33 -0
- basekit/http/clients/curl_cffi.py +99 -0
- basekit/http/clients/httpx.py +100 -0
- basekit/http/schema.py +111 -0
- basekit/http/utils.py +31 -0
- basekit/limiter.py +179 -0
- basekit/py.typed +0 -0
- basekit/utils/batch.py +21 -0
- basekit/utils/console.py +26 -0
- basekit/utils/html.py +54 -0
- basekit/utils/jinja.py +20 -0
- basekit/utils/markdown.py +38 -0
- basekit/utils/mime.py +39 -0
- basekit/utils/misc.py +35 -0
- python_basekit-0.0.11.dist-info/METADATA +46 -0
- python_basekit-0.0.11.dist-info/RECORD +27 -0
- python_basekit-0.0.11.dist-info/WHEEL +4 -0
basekit/limiter.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from collections import deque
|
|
4
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
|
5
|
+
from contextlib import nullcontext
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Annotated, Any, Literal, Self, override
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncRateLimiter(BaseModel):
|
|
13
|
+
limit: Annotated[int, Field(gt=0)]
|
|
14
|
+
window: Annotated[float, Field(gt=0)] = 60.0
|
|
15
|
+
|
|
16
|
+
_timestamps: deque[float] = PrivateAttr()
|
|
17
|
+
_lock: asyncio.Lock = PrivateAttr()
|
|
18
|
+
_waiters: deque[asyncio.Future[None]] = PrivateAttr()
|
|
19
|
+
_wake_task: asyncio.Task[None] | None = PrivateAttr()
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
def model_post_init(self, context) -> None:
|
|
23
|
+
self._timestamps = deque()
|
|
24
|
+
self._lock = asyncio.Lock()
|
|
25
|
+
self._waiters = deque()
|
|
26
|
+
self._wake_task = None
|
|
27
|
+
|
|
28
|
+
def _evict(self, now: float, /) -> None:
|
|
29
|
+
cutoff: float = now - self.window
|
|
30
|
+
dq: deque[float] = self._timestamps
|
|
31
|
+
|
|
32
|
+
while dq and dq[0] < cutoff:
|
|
33
|
+
dq.popleft()
|
|
34
|
+
|
|
35
|
+
def _drain_waiters_unlocked(self, now: float, /) -> None:
|
|
36
|
+
self._evict(now)
|
|
37
|
+
|
|
38
|
+
while self._waiters and len(self._timestamps) < self.limit:
|
|
39
|
+
waiter: asyncio.Future[None] = self._waiters.popleft()
|
|
40
|
+
if waiter.done():
|
|
41
|
+
continue
|
|
42
|
+
self._timestamps.append(now)
|
|
43
|
+
waiter.set_result(None)
|
|
44
|
+
|
|
45
|
+
def _reschedule_wake_unlocked(self, now: float, /) -> None:
|
|
46
|
+
self._drain_waiters_unlocked(now)
|
|
47
|
+
|
|
48
|
+
if not self._waiters:
|
|
49
|
+
if self._wake_task is not None and not self._wake_task.done():
|
|
50
|
+
self._wake_task.cancel()
|
|
51
|
+
self._wake_task = None
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
if self._wake_task is not None and not self._wake_task.done():
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
wait: float = max(self._timestamps[0] + self.window - now, 0.0)
|
|
58
|
+
self._wake_task = asyncio.create_task(coro=self._wake_after(wait))
|
|
59
|
+
|
|
60
|
+
async def _wake_after(self, wait: float, /) -> None:
|
|
61
|
+
try:
|
|
62
|
+
await asyncio.sleep(delay=wait)
|
|
63
|
+
except asyncio.CancelledError:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
async with self._lock:
|
|
67
|
+
if self._wake_task is not asyncio.current_task():
|
|
68
|
+
return
|
|
69
|
+
self._wake_task = None
|
|
70
|
+
self._reschedule_wake_unlocked(time.monotonic())
|
|
71
|
+
|
|
72
|
+
async def _acquire(self) -> None:
|
|
73
|
+
async with self._lock:
|
|
74
|
+
now: float = time.monotonic()
|
|
75
|
+
self._evict(now)
|
|
76
|
+
if not self._waiters and len(self._timestamps) < self.limit:
|
|
77
|
+
self._timestamps.append(now)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
waiter: asyncio.Future[None] = asyncio.get_running_loop().create_future()
|
|
81
|
+
self._waiters.append(waiter)
|
|
82
|
+
self._reschedule_wake_unlocked(now)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
await waiter
|
|
86
|
+
except asyncio.CancelledError:
|
|
87
|
+
async with self._lock:
|
|
88
|
+
try:
|
|
89
|
+
self._waiters.remove(waiter)
|
|
90
|
+
except ValueError:
|
|
91
|
+
pass
|
|
92
|
+
self._reschedule_wake_unlocked(time.monotonic())
|
|
93
|
+
raise
|
|
94
|
+
|
|
95
|
+
async def __aenter__(self) -> Self:
|
|
96
|
+
await self._acquire()
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
def __call__[T, **P](
|
|
103
|
+
self, func: Callable[P, Awaitable[T]], /
|
|
104
|
+
) -> Callable[P, Coroutine[Any, Any, T]]:
|
|
105
|
+
@wraps(wrapped=func)
|
|
106
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
107
|
+
async with self:
|
|
108
|
+
return await func(*args, **kwargs)
|
|
109
|
+
|
|
110
|
+
return wrapper
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AsyncConcurrencyLimiter(BaseModel):
|
|
114
|
+
limit: Annotated[int, Field(gt=0)]
|
|
115
|
+
|
|
116
|
+
_semaphore: asyncio.Semaphore = PrivateAttr()
|
|
117
|
+
|
|
118
|
+
@override
|
|
119
|
+
def model_post_init(self, context) -> None:
|
|
120
|
+
self._semaphore = asyncio.Semaphore(value=self.limit)
|
|
121
|
+
|
|
122
|
+
async def __aenter__(self) -> Self:
|
|
123
|
+
await self._semaphore.__aenter__()
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
127
|
+
await self._semaphore.__aexit__(exc_type=exc_type, exc=exc, tb=tb)
|
|
128
|
+
|
|
129
|
+
def __call__[T, **P](
|
|
130
|
+
self, func: Callable[P, Awaitable[T]], /
|
|
131
|
+
) -> Callable[P, Coroutine[Any, Any, T]]:
|
|
132
|
+
@wraps(wrapped=func)
|
|
133
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
134
|
+
async with self:
|
|
135
|
+
return await func(*args, **kwargs)
|
|
136
|
+
|
|
137
|
+
return wrapper
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class AsyncLimiter(BaseModel):
|
|
141
|
+
concurrency_limit: Annotated[int | None, Field(gt=0)]
|
|
142
|
+
rate_limit: Annotated[int | None, Field(gt=0)]
|
|
143
|
+
rate_window: Annotated[float, Field(gt=0)] = 60.0
|
|
144
|
+
|
|
145
|
+
_concurrency_limiter: AsyncConcurrencyLimiter | nullcontext = PrivateAttr()
|
|
146
|
+
_rate_limiter: AsyncRateLimiter | nullcontext = PrivateAttr()
|
|
147
|
+
|
|
148
|
+
@override
|
|
149
|
+
def model_post_init(self, context) -> None:
|
|
150
|
+
self._concurrency_limiter = (
|
|
151
|
+
AsyncConcurrencyLimiter(limit=self.concurrency_limit)
|
|
152
|
+
if self.concurrency_limit is not None
|
|
153
|
+
else nullcontext()
|
|
154
|
+
)
|
|
155
|
+
self._rate_limiter = (
|
|
156
|
+
AsyncRateLimiter(limit=self.rate_limit, window=self.rate_window)
|
|
157
|
+
if self.rate_limit is not None
|
|
158
|
+
else nullcontext()
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def __aenter__(self) -> Self:
|
|
162
|
+
await self._concurrency_limiter.__aenter__()
|
|
163
|
+
await self._rate_limiter.__aenter__()
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
async def __aexit__(self, exc_type, exc, tb) -> Literal[False]:
|
|
167
|
+
await self._concurrency_limiter.__aexit__(exc_type, exc, tb)
|
|
168
|
+
await self._rate_limiter.__aexit__(exc_type, exc, tb)
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
def __call__[T, **P](
|
|
172
|
+
self, func: Callable[P, Awaitable[T]], /
|
|
173
|
+
) -> Callable[P, Coroutine[Any, Any, T]]:
|
|
174
|
+
@wraps(wrapped=func)
|
|
175
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
176
|
+
async with self:
|
|
177
|
+
return await func(*args, **kwargs)
|
|
178
|
+
|
|
179
|
+
return wrapper
|
basekit/py.typed
ADDED
|
File without changes
|
basekit/utils/batch.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Awaitable
|
|
3
|
+
|
|
4
|
+
import logfire
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def run_batch[T](coros: list[Awaitable[T]], /, *, strict: bool) -> list[T]:
|
|
8
|
+
if strict:
|
|
9
|
+
return await asyncio.gather(*coros)
|
|
10
|
+
|
|
11
|
+
results: list[T | BaseException] = await asyncio.gather(
|
|
12
|
+
*coros, return_exceptions=True
|
|
13
|
+
)
|
|
14
|
+
instances: list[T] = [r for r in results if not isinstance(r, BaseException)]
|
|
15
|
+
errors: list[BaseException] = [r for r in results if isinstance(r, BaseException)]
|
|
16
|
+
if errors:
|
|
17
|
+
exception: BaseExceptionGroup[BaseException] = BaseExceptionGroup(
|
|
18
|
+
f"批量运行协程失败,{len(instances)}成功,{len(errors)}失败", errors
|
|
19
|
+
)
|
|
20
|
+
logfire.exception(str(object=exception), _exc_info=exception)
|
|
21
|
+
return instances
|
basekit/utils/console.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table, box
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def print_table[T: BaseModel](
|
|
7
|
+
console: Console, instances: list[T], /, *, title: str | None = None
|
|
8
|
+
) -> None:
|
|
9
|
+
table = Table(title=title, box=box.SQUARE_DOUBLE_HEAD, show_lines=True)
|
|
10
|
+
|
|
11
|
+
if not instances:
|
|
12
|
+
table.add_column(header="无数据", justify="center", overflow="fold")
|
|
13
|
+
table.add_row("无数据")
|
|
14
|
+
console.print(table)
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
cls: type[T] = instances[0].__class__
|
|
18
|
+
for field in cls.model_fields.values():
|
|
19
|
+
table.add_column(header=field.description or "-", overflow="fold")
|
|
20
|
+
for instance in instances:
|
|
21
|
+
fields: list[str] = [
|
|
22
|
+
str(object=getattr(instance, field) or "-")
|
|
23
|
+
for field in cls.model_fields.keys()
|
|
24
|
+
]
|
|
25
|
+
table.add_row(*fields)
|
|
26
|
+
console.print(table)
|
basekit/utils/html.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from urllib.parse import urljoin, urlparse
|
|
2
|
+
|
|
3
|
+
from bs4 import BeautifulSoup, Tag
|
|
4
|
+
from bs4.element import AttributeValueList
|
|
5
|
+
|
|
6
|
+
from basekit.utils.misc import require_non_null, require_truthy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def join_url(url: str, base: str, /) -> str:
|
|
10
|
+
if urlparse(url=url).scheme:
|
|
11
|
+
return url
|
|
12
|
+
if url.startswith("//"):
|
|
13
|
+
return f"{urlparse(url=base).scheme}:{url}"
|
|
14
|
+
return urljoin(base=base, url=url)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_attr(element: Tag, key: str, /) -> str | None:
|
|
18
|
+
value: str | AttributeValueList | None = element.get(key=key)
|
|
19
|
+
if value is None:
|
|
20
|
+
return None
|
|
21
|
+
if isinstance(value, str):
|
|
22
|
+
return value
|
|
23
|
+
return " ".join(value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def has_class(element: Tag, class_name: str, /) -> bool:
|
|
27
|
+
classes: str | None = extract_attr(element, "class")
|
|
28
|
+
if classes is None:
|
|
29
|
+
return False
|
|
30
|
+
return class_name in classes.split(sep=" ")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def select_one(element: BeautifulSoup, selector: str, /) -> Tag:
|
|
34
|
+
result: Tag | None = element.select_one(selector=selector)
|
|
35
|
+
return require_non_null(result)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def select_many(element: BeautifulSoup, selector: str, /) -> list[Tag]:
|
|
39
|
+
results: list[Tag] = element.select(selector=selector)
|
|
40
|
+
return require_truthy(results)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def combine_htmls(*htmls: str) -> str:
|
|
44
|
+
contents: list[str] = []
|
|
45
|
+
for html in htmls:
|
|
46
|
+
soup = BeautifulSoup(markup=html, features="lxml")
|
|
47
|
+
for element in require_non_null(soup.body).children:
|
|
48
|
+
contents.append(str(object=element))
|
|
49
|
+
html: str = "".join(contents)
|
|
50
|
+
return BeautifulSoup(markup=html, features="lxml").decode()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_mime(content_type: str, /) -> str:
|
|
54
|
+
return content_type.split(sep=";", maxsplit=1)[0].strip().lower()
|
basekit/utils/jinja.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from jinja2 import Environment
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def split_sentences(text: str) -> list[str]:
|
|
7
|
+
if not text:
|
|
8
|
+
return []
|
|
9
|
+
return re.findall(pattern=r"[^。]+。?", string=text)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
MARKDOWN_ENV: Environment = Environment(
|
|
13
|
+
autoescape=False, keep_trailing_newline=True, lstrip_blocks=True, trim_blocks=True
|
|
14
|
+
)
|
|
15
|
+
MARKDOWN_ENV.policies["json.dumps_kwargs"]["ensure_ascii"] = False
|
|
16
|
+
MARKDOWN_ENV.filters["split_sentences"] = split_sentences
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def render_template(template: str, /, **kwargs) -> str:
|
|
20
|
+
return MARKDOWN_ENV.from_string(source=template).render(**kwargs)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from flowmark import reformat_text
|
|
2
|
+
from flowmark.linewrapping.markdown_filling import ListSpacing
|
|
3
|
+
from markdown_it import MarkdownIt
|
|
4
|
+
from markdown_it.tree import SyntaxTreeNode
|
|
5
|
+
from markdownify import markdownify
|
|
6
|
+
|
|
7
|
+
PARSER: MarkdownIt = (
|
|
8
|
+
MarkdownIt(config="commonmark")
|
|
9
|
+
.enable(names=["linkify", "replacements", "smartquotes"])
|
|
10
|
+
.enable(names=["table", "strikethrough"])
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def format_markdown(markdown: str, /) -> str:
|
|
15
|
+
return reformat_text(
|
|
16
|
+
text=markdown, width=65535, semantic=False, list_spacing=ListSpacing.tight
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def convert_markdown(html: str, /) -> str:
|
|
21
|
+
markdown: str = markdownify(
|
|
22
|
+
html=html, heading_style="ATX", bullets="-", wrap_width=65535
|
|
23
|
+
)
|
|
24
|
+
return format_markdown(markdown)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_markdown(markdown: str, /) -> list[tuple[str, str]]:
|
|
28
|
+
lines: list[str] = markdown.splitlines(keepends=True)
|
|
29
|
+
root = SyntaxTreeNode(tokens=PARSER.parse(src=markdown))
|
|
30
|
+
blocks: list[tuple[str, str]] = []
|
|
31
|
+
for node in root.children:
|
|
32
|
+
if node.map is None:
|
|
33
|
+
continue
|
|
34
|
+
content: str = "".join(lines[node.map[0] : node.map[1]]).strip()
|
|
35
|
+
if not content:
|
|
36
|
+
continue
|
|
37
|
+
blocks.append((content, node.type))
|
|
38
|
+
return blocks
|
basekit/utils/mime.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from mimetypes import guess_extension, guess_type
|
|
2
|
+
|
|
3
|
+
from filetype import Type
|
|
4
|
+
from filetype import guess as guess_filetype
|
|
5
|
+
|
|
6
|
+
DEFAULT_MIME = "application/octet-stream"
|
|
7
|
+
DEFAULT_SUFFIX = ".bin"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def guess_mime(
|
|
11
|
+
*, data: bytes | None = None, mime: str | None = None, suffix: str | None = None
|
|
12
|
+
) -> str:
|
|
13
|
+
if data:
|
|
14
|
+
file_type: Type | None = guess_filetype(obj=data)
|
|
15
|
+
if file_type:
|
|
16
|
+
return file_type.mime
|
|
17
|
+
if mime:
|
|
18
|
+
return mime
|
|
19
|
+
if suffix:
|
|
20
|
+
suffix_mime: str | None = guess_type(url=f"test{suffix}")[0]
|
|
21
|
+
if suffix_mime:
|
|
22
|
+
return suffix_mime
|
|
23
|
+
return DEFAULT_MIME
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def guess_suffix(
|
|
27
|
+
*, data: bytes | None = None, mime: str | None = None, suffix: str | None = None
|
|
28
|
+
) -> str:
|
|
29
|
+
if data:
|
|
30
|
+
file_type: Type | None = guess_filetype(obj=data)
|
|
31
|
+
if file_type:
|
|
32
|
+
return f".{file_type.extension}"
|
|
33
|
+
if mime:
|
|
34
|
+
mime_suffix: str | None = guess_extension(type=mime)
|
|
35
|
+
if mime_suffix:
|
|
36
|
+
return mime_suffix
|
|
37
|
+
if suffix:
|
|
38
|
+
return suffix
|
|
39
|
+
return DEFAULT_SUFFIX
|
basekit/utils/misc.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def require_non_null[T](v: T | None, /) -> T:
|
|
6
|
+
if v is None:
|
|
7
|
+
raise ValueError(f"期望非空值: {v}")
|
|
8
|
+
return v
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def require_truthy[T](v: T | None, /) -> T:
|
|
12
|
+
if not v:
|
|
13
|
+
raise ValueError(f"期望真值: {v}")
|
|
14
|
+
return v
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def remove_space(s: str, /) -> str:
|
|
18
|
+
return " ".join(s.split())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def recursive_merge(*args: Mapping | None) -> dict:
|
|
22
|
+
result: dict = {}
|
|
23
|
+
|
|
24
|
+
for mapping in args:
|
|
25
|
+
if mapping is None:
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
for key, value in mapping.items():
|
|
29
|
+
current: Any | None = result.get(key)
|
|
30
|
+
if isinstance(current, Mapping) and isinstance(value, Mapping):
|
|
31
|
+
result[key] = recursive_merge(current, value)
|
|
32
|
+
else:
|
|
33
|
+
result[key] = value
|
|
34
|
+
|
|
35
|
+
return result
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: python-basekit
|
|
3
|
+
Version: 0.0.11
|
|
4
|
+
Summary: Python Base Kit
|
|
5
|
+
Author: YanSH
|
|
6
|
+
Author-email: YanSH <yansh97@foxmail.com>
|
|
7
|
+
Requires-Dist: aiosqlite>=0.22.1
|
|
8
|
+
Requires-Dist: beautifulsoup4>=4.14.3
|
|
9
|
+
Requires-Dist: curl-cffi>=0.14.0
|
|
10
|
+
Requires-Dist: filetype>=1.2.0
|
|
11
|
+
Requires-Dist: flowmark>=0.6.5
|
|
12
|
+
Requires-Dist: greenlet>=3.3.2
|
|
13
|
+
Requires-Dist: httpx[http2]>=0.28.1
|
|
14
|
+
Requires-Dist: jinja2>=3.1.6
|
|
15
|
+
Requires-Dist: logfire>=4.27.0
|
|
16
|
+
Requires-Dist: lxml>=6.0.2
|
|
17
|
+
Requires-Dist: markdown-it-py>=4.0.0
|
|
18
|
+
Requires-Dist: markdownify>=1.2.2
|
|
19
|
+
Requires-Dist: pydantic>=2.12.5
|
|
20
|
+
Requires-Dist: pydantic-settings>=2.13.1
|
|
21
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
22
|
+
Requires-Dist: rich>=14.3.3
|
|
23
|
+
Requires-Dist: sqlalchemy>=2.0.48
|
|
24
|
+
Requires-Dist: tenacity>=9.1.4
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Base Kit
|
|
29
|
+
|
|
30
|
+
一个面向内部使用的 Python 基础工具包。
|
|
31
|
+
|
|
32
|
+
## 环境
|
|
33
|
+
|
|
34
|
+
Python `>=3.12`
|
|
35
|
+
|
|
36
|
+
## 安装
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add python-basekit
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 使用
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import basekit
|
|
46
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
basekit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
basekit/ai/clients/anthropic.py,sha256=iozRCRs3VC-JfZCRyiVPEIJ2vW-X4-IW0IcaCt5d5es,7620
|
|
3
|
+
basekit/ai/clients/dashscope.py,sha256=QD_lSmSJYGkRtOOjg-C5CBHh3rN7tWf_ySHdnPWQ0js,4869
|
|
4
|
+
basekit/ai/clients/gemini.py,sha256=MjM6JWzt7V4_zB3XR3_JtDRBzerUuDUjkRrsctiLTuQ,10795
|
|
5
|
+
basekit/ai/clients/openai.py,sha256=pDypO4DaGygZVLupoBFYFfC_9qbA5rLVVs4GFK1_oVY,15255
|
|
6
|
+
basekit/ai/schema.py,sha256=dTy3qff6YOUGBJN-fhhLjFEdcSGl_Agt35l1pb25JgU,5103
|
|
7
|
+
basekit/ai/utils.py,sha256=P1VtktxlGkQn0suqS4xU5McG1zBHt8Ft75h19Mpv0II,1926
|
|
8
|
+
basekit/cache/clients/sqlite.py,sha256=MpLInsD-xdE7-sIwAIJwhVrtMc2ASsTgthndo9mPumk,2981
|
|
9
|
+
basekit/cache/schema.py,sha256=VYsE6nE5zARp0EYDaF2eqmVPh0qJkGkkP3-hGwq-P4A,1641
|
|
10
|
+
basekit/cache/utils.py,sha256=hNQ7uqIadkMe6e6469UZXzCDYrPb0n52xwThEvFUQFk,3758
|
|
11
|
+
basekit/database.py,sha256=LRt7rJGlg-le_SHKEu7WfnGjPd32yb5GyB_WswoIErE,1003
|
|
12
|
+
basekit/http/clients/curl_cffi.py,sha256=G4FyvVVP3TR_K_6UZ4mGJaM--IYuBqzL0y1lCWRbH6M,3549
|
|
13
|
+
basekit/http/clients/httpx.py,sha256=NgWlBmx164Kjmh07rk86gzZI4BhFn2tr_dh6VN9Li28,3501
|
|
14
|
+
basekit/http/schema.py,sha256=P_QwrHWjsUEXmqx-rZXibrutA83HuOsVeH5OJfpB2bs,3061
|
|
15
|
+
basekit/http/utils.py,sha256=SRLexTifocJ98Utxb1YSHoM5LBFa2r9sbCSeCDHJcvQ,941
|
|
16
|
+
basekit/limiter.py,sha256=18CuaAppqcZVQUg7jHjC_ZnN-9Vx_0vB0VMqI4J2ekA,5885
|
|
17
|
+
basekit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
basekit/utils/batch.py,sha256=HNTc-12e3AkPC5dMUH2FbSinba2ZPxg9suPQeJd3utM,774
|
|
19
|
+
basekit/utils/console.py,sha256=LPx6y0qlCd927z6FNqs6_rdva9QIFOMDrw4OcrY4rDE,867
|
|
20
|
+
basekit/utils/html.py,sha256=RYg1oTe1J54eVNeiNiy8oMJZo-r4Tnz9yjdxU3Q4YKw,1676
|
|
21
|
+
basekit/utils/jinja.py,sha256=8eoKz4dF4iSyvi8yInvtpq8_UYYul2jBVQGM1SxxJk0,568
|
|
22
|
+
basekit/utils/markdown.py,sha256=lyDuvPhDxNUQzGWi2uP-F7qnaje9oizITH7FbZFikCo,1221
|
|
23
|
+
basekit/utils/mime.py,sha256=nOMeAtDkVQP1rPGM5P-kkP0GeLhWIdhKMgs4paNcr6I,1070
|
|
24
|
+
basekit/utils/misc.py,sha256=c5tkhPvyBpoHsvH4X7uuuit98AYEgAWwGU2EZyB4AGw,833
|
|
25
|
+
python_basekit-0.0.11.dist-info/WHEEL,sha256=AKeAJsuKQDVE1yEMr94YEC7FtwPuFZZw2DmbXPmgtaE,81
|
|
26
|
+
python_basekit-0.0.11.dist-info/METADATA,sha256=tkrxndeuUG_XmAyw8PjfL9PCy72rqWK-5xcZ8IU73XE,979
|
|
27
|
+
python_basekit-0.0.11.dist-info/RECORD,,
|