pytilpack 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 aki.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.1
2
+ Name: pytilpack
3
+ Version: 0.1.0
4
+ Summary: Python Utility Pack
5
+ Home-page: https://github.com/ak110/pytilpack
6
+ License: MIT
7
+ Author: aki.
8
+ Author-email: mark@aur.ll.to
9
+ Requires-Python: >=3.11,<4.0
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Provides-Extra: all
19
+ Provides-Extra: flask
20
+ Provides-Extra: openai
21
+ Provides-Extra: sqlalchemy
22
+ Provides-Extra: tqdm
23
+ Requires-Dist: flask (>=2.2) ; extra == "all" or extra == "flask"
24
+ Requires-Dist: openai (>=1.25) ; extra == "all" or extra == "openai"
25
+ Requires-Dist: sqlalchemy (>=2.0) ; extra == "all" or extra == "sqlalchemy"
26
+ Requires-Dist: tqdm (>=4.66) ; extra == "all" or extra == "tqdm"
27
+ Description-Content-Type: text/markdown
28
+
29
+ # pytilpack
30
+
31
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
32
+ [![Lint&Test](https://github.com/ak110/pytilpack/actions/workflows/python-app.yml/badge.svg)](https://github.com/ak110/pytilpack/actions/workflows/python-app.yml)
33
+ [![PyPI version](https://badge.fury.io/py/pytilpack.svg)](https://badge.fury.io/py/pytilpack)
34
+
35
+ Pythonの各種ライブラリのユーティリティ集。
36
+
37
+ ## インストール
38
+
39
+ ```bash
40
+ pip install pytilpack
41
+ ```
42
+
@@ -0,0 +1,13 @@
1
+ # pytilpack
2
+
3
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
4
+ [![Lint&Test](https://github.com/ak110/pytilpack/actions/workflows/python-app.yml/badge.svg)](https://github.com/ak110/pytilpack/actions/workflows/python-app.yml)
5
+ [![PyPI version](https://badge.fury.io/py/pytilpack.svg)](https://badge.fury.io/py/pytilpack)
6
+
7
+ Pythonの各種ライブラリのユーティリティ集。
8
+
9
+ ## インストール
10
+
11
+ ```bash
12
+ pip install pytilpack
13
+ ```
@@ -0,0 +1,80 @@
1
+ [tool.poetry]
2
+ name = "pytilpack"
3
+ version = "0.1.0" # using poetry-dynamic-versioning
4
+ description = "Python Utility Pack"
5
+ license = "MIT"
6
+ authors = ["aki. <mark@aur.ll.to>"]
7
+ readme = "README.md"
8
+ homepage = "https://github.com/ak110/pytilpack"
9
+ classifiers = [
10
+ "Environment :: Console",
11
+ "Intended Audience :: Developers",
12
+ "Operating System :: OS Independent",
13
+ "Programming Language :: Python :: 3 :: Only",
14
+ ]
15
+
16
+ [tool.poetry-dynamic-versioning]
17
+ enable = false
18
+ style = "pep440"
19
+
20
+ [tool.poetry.dependencies]
21
+ python = ">=3.11,<4.0"
22
+ flask = {version = ">=2.2", optional = true}
23
+ openai = {version = ">=1.25", optional = true}
24
+ sqlalchemy = {version = ">=2.0", optional = true}
25
+ tqdm = {version = ">=4.66", optional = true}
26
+
27
+ [tool.poetry.group.dev.dependencies]
28
+ pyfltr = "*"
29
+
30
+ [tool.poetry.extras]
31
+ all = [
32
+ "flask",
33
+ "openai",
34
+ "sqlalchemy",
35
+ "tqdm",
36
+ ]
37
+ flask = ["flask"]
38
+ openai = ["openai"]
39
+ sqlalchemy = ["sqlalchemy"]
40
+ tqdm = ["tqdm"]
41
+
42
+ [build-system]
43
+ requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
44
+ build-backend = "poetry_dynamic_versioning.backend"
45
+
46
+ [tool.pyfltr]
47
+ pyupgrade-args = ["--py311-plus"]
48
+ pylint-args = ["--jobs=4"]
49
+
50
+ [tool.isort]
51
+ # https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#isort
52
+ # https://pycqa.github.io/isort/docs/configuration/options.html
53
+ profile = "black"
54
+
55
+ [tool.black]
56
+ # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html
57
+ target-version = ['py311']
58
+ skip-magic-trailing-comma = true
59
+
60
+ [tool.flake8]
61
+ # https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8
62
+ # https://flake8.pycqa.org/en/latest/user/configuration.html
63
+ max-line-length = 128
64
+ extend-ignore = "E203,"
65
+
66
+ [tool.mypy]
67
+ # https://mypy.readthedocs.io/en/stable/config_file.html
68
+ allow_redefinition = true
69
+ check_untyped_defs = true
70
+ ignore_missing_imports = true
71
+ strict_optional = true
72
+ strict_equality = true
73
+ warn_no_return = true
74
+ warn_redundant_casts = true
75
+ warn_unused_configs = true
76
+ show_error_codes = true
77
+
78
+ [tool.pytest.ini_options]
79
+ # https://docs.pytest.org/en/latest/reference/reference.html#ini-options-ref
80
+ addopts = "--showlocals -p no:cacheprovider"
File without changes
@@ -0,0 +1,61 @@
1
+ """Flask関連のユーティリティ。"""
2
+
3
+ import base64
4
+ import logging
5
+ import pathlib
6
+ import secrets
7
+ import urllib.parse
8
+
9
+ import flask
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def generate_secret_key(cache_path: str | pathlib.Path) -> bytes:
15
+ """シークレットキーの作成/取得。
16
+
17
+ 既にcache_pathに保存済みならそれを返し、でなくば作成する。
18
+
19
+ """
20
+ cache_path = pathlib.Path(cache_path)
21
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
22
+ with cache_path.open("a+b") as secret:
23
+ secret.seek(0)
24
+ secret_key = secret.read()
25
+ if not secret_key:
26
+ secret_key = secrets.token_bytes()
27
+ secret.write(secret_key)
28
+ secret.flush()
29
+ return secret_key
30
+
31
+
32
+ def data_url(data: bytes, mime_type: str) -> str:
33
+ """小さい画像などのバイナリデータをURLに埋め込んだものを作って返す。
34
+
35
+ Args:
36
+ data: 埋め込むデータ
37
+ mime_type: 例:'image/png'
38
+
39
+ """
40
+ b64 = base64.b64encode(data).decode("ascii")
41
+ return f"data:{mime_type};base64,{b64}"
42
+
43
+
44
+ def get_next_url() -> str:
45
+ """flask_loginのnextパラメータ用のURLを返す。"""
46
+ path = flask.request.script_root + flask.request.path
47
+ query_string = flask.request.query_string.decode("utf-8")
48
+ next_ = f"{path}?{query_string}" if query_string else path
49
+ return next_
50
+
51
+
52
+ def get_safe_url(target: str, host_url: str, default_url: str) -> str:
53
+ """ログイン時のリダイレクトとして安全なURLを返す。"""
54
+ if target is None or target == "":
55
+ return default_url
56
+ ref_url = urllib.parse.urlparse(host_url)
57
+ test_url = urllib.parse.urlparse(urllib.parse.urljoin(host_url, target))
58
+ if test_url.scheme not in ("http", "https") or ref_url.netloc != test_url.netloc:
59
+ logger.warning(f"Invalid next url: {target}")
60
+ return default_url
61
+ return target
@@ -0,0 +1,121 @@
1
+ """OpenAI Python Library用のユーティリティ集。"""
2
+
3
+ import openai
4
+ import openai.types.chat
5
+
6
+ import pytilpack.python_
7
+
8
+
9
+ def gather_chunks(
10
+ chunks: list[openai.types.chat.ChatCompletionChunk],
11
+ ) -> openai.types.chat.ChatCompletion:
12
+ """ストリーミングのチャンクを結合する。"""
13
+ max_choices = max(len(chunk.choices) for chunk in chunks)
14
+ choices = [_make_choice(chunks, i) for i in range(max_choices)]
15
+ return openai.types.chat.ChatCompletion(
16
+ id=chunks[0].id,
17
+ choices=choices,
18
+ created=chunks[0].created,
19
+ model=chunks[0].model,
20
+ object="chat.completion",
21
+ system_fingerprint=chunks[0].system_fingerprint,
22
+ )
23
+
24
+
25
+ def _make_choice(
26
+ chunks: list[openai.types.chat.ChatCompletionChunk], i: int
27
+ ) -> openai.types.chat.chat_completion.Choice:
28
+ """ストリーミングのチャンクからChoiceを作成する。"""
29
+ logprobs = pytilpack.python_.coalesce(
30
+ c.choices[i].logprobs for c in chunks if len(c.choices) >= i
31
+ )
32
+ return openai.types.chat.chat_completion.Choice(
33
+ finish_reason=pytilpack.python_.coalesce(
34
+ (c.choices[i].finish_reason for c in chunks if len(c.choices) >= i), "stop"
35
+ ),
36
+ index=i,
37
+ logprobs=(
38
+ None
39
+ if logprobs is None
40
+ else openai.types.chat.chat_completion.ChoiceLogprobs(
41
+ content=logprobs.content
42
+ )
43
+ ),
44
+ message=openai.types.chat.ChatCompletionMessage(
45
+ content="".join(
46
+ pytilpack.python_.remove_none(
47
+ c.choices[i].delta.content for c in chunks if len(c.choices) >= i
48
+ )
49
+ ),
50
+ # role=pytilpack.python_.coalesce(
51
+ # (c.choices[i].delta.role for c in chunks if len(c.choices) >= i),
52
+ # "assistant",
53
+ # ),
54
+ role="assistant",
55
+ function_call=_make_function_call(
56
+ pytilpack.python_.remove_none(
57
+ c.choices[i].delta.function_call
58
+ for c in chunks
59
+ if len(c.choices) >= i
60
+ )
61
+ ),
62
+ tool_calls=_make_tool_calls(
63
+ pytilpack.python_.remove_none(
64
+ c.choices[i].delta.tool_calls for c in chunks if len(c.choices) >= i
65
+ )
66
+ ),
67
+ ),
68
+ )
69
+
70
+
71
+ def _make_function_call(
72
+ deltas: list[openai.types.chat.chat_completion_chunk.ChoiceDeltaFunctionCall],
73
+ ) -> openai.types.chat.chat_completion_message.FunctionCall | None:
74
+ """ChoiceDeltaFunctionCallを作成する。"""
75
+ if len(deltas) == 0:
76
+ return None
77
+ return openai.types.chat.chat_completion_message.FunctionCall(
78
+ arguments="".join(d.arguments for d in deltas if d.arguments is not None),
79
+ name="".join(d.name for d in deltas if d.name is not None),
80
+ )
81
+
82
+
83
+ def _make_tool_calls(
84
+ deltas_list: list[
85
+ list[openai.types.chat.chat_completion_chunk.ChoiceDeltaToolCall]
86
+ ],
87
+ ) -> (
88
+ list[openai.types.chat.chat_completion_message.ChatCompletionMessageToolCall] | None
89
+ ):
90
+ """list[ChoiceDeltaToolCall]を作成する。"""
91
+ if len(deltas_list) == 0:
92
+ return None
93
+ max_tool_calls = max(len(deltas) for deltas in deltas_list)
94
+ if max_tool_calls == 0:
95
+ return None
96
+ return [_make_tool_call(deltas_list, i) for i in range(max_tool_calls)]
97
+
98
+
99
+ def _make_tool_call(
100
+ deltas_list: list[
101
+ list[openai.types.chat.chat_completion_chunk.ChoiceDeltaToolCall]
102
+ ],
103
+ i: int,
104
+ ) -> openai.types.chat.chat_completion_message.ChatCompletionMessageToolCall:
105
+ """ChoiceDeltaToolCallを作成する。"""
106
+ deltas_list = [deltas for deltas in deltas_list if len(deltas) >= i]
107
+ functions = pytilpack.python_.remove_none(
108
+ deltas[i].function for deltas in deltas_list
109
+ )
110
+ return openai.types.chat.chat_completion_message.ChatCompletionMessageToolCall(
111
+ id=pytilpack.python_.coalesce((deltas[i].id for deltas in deltas_list), ""),
112
+ function=openai.types.chat.chat_completion_message_tool_call.Function(
113
+ arguments="".join(
114
+ pytilpack.python_.remove_none(f.arguments for f in functions)
115
+ ),
116
+ name="".join(pytilpack.python_.remove_none(f.name for f in functions)),
117
+ ),
118
+ type=pytilpack.python_.coalesce(
119
+ (deltas[i].type for deltas in deltas_list), "function"
120
+ ),
121
+ )
@@ -0,0 +1,28 @@
1
+ """Pythonのユーティリティ集。"""
2
+
3
+ import typing
4
+
5
+ T = typing.TypeVar("T")
6
+
7
+
8
+ @typing.overload
9
+ def coalesce(iterable: typing.Iterable[T | None], default: None = None) -> T:
10
+ pass
11
+
12
+
13
+ @typing.overload
14
+ def coalesce(iterable: typing.Iterable[T | None], default: T) -> T:
15
+ pass
16
+
17
+
18
+ def coalesce(iterable: typing.Iterable[T | None], default: T | None = None) -> T | None:
19
+ """Noneでない最初の要素を取得する。"""
20
+ for item in iterable:
21
+ if item is not None:
22
+ return item
23
+ return default
24
+
25
+
26
+ def remove_none(iterable: typing.Iterable[T | None]) -> list[T]:
27
+ """Noneを除去する。"""
28
+ return [item for item in iterable if item is not None]
@@ -0,0 +1,76 @@
1
+ """SQLAlchemy用のユーティリティ集。"""
2
+
3
+ import logging
4
+ import time
5
+ import typing
6
+
7
+ import sqlalchemy
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def register_ping():
13
+ """コネクションプールの切断対策。"""
14
+
15
+ @sqlalchemy.event.listens_for(sqlalchemy.pool.Pool, "checkout")
16
+ def _ping_connection(dbapi_connection, connection_record, connection_proxy):
17
+ """コネクションプールの切断対策。"""
18
+ _ = connection_record, connection_proxy # noqa
19
+ cursor = dbapi_connection.cursor()
20
+ try:
21
+ cursor.execute("SELECT 1")
22
+ except Exception as e:
23
+ raise sqlalchemy.exc.DisconnectionError() from e
24
+ finally:
25
+ cursor.close()
26
+
27
+
28
+ class IDMixin:
29
+ """models.Class.query.get()がdeprecatedになるため"""
30
+
31
+ @classmethod
32
+ def get_by_id(
33
+ cls: type[typing.Self], id_: int, for_update: bool = False
34
+ ) -> typing.Self | None:
35
+ """IDを元にインスタンスを取得。"""
36
+ q = cls.query.filter(cls.id == id_) # type: ignore
37
+ if for_update:
38
+ q = q.with_for_update()
39
+ return q.one_or_none()
40
+
41
+
42
+ def wait_for_connection(url: str, timeout: float = 10.0) -> None:
43
+ """DBに接続可能になるまで待機する。"""
44
+ failed = False
45
+ start_time = time.time()
46
+ while True:
47
+ try:
48
+ engine = sqlalchemy.create_engine(url)
49
+ try:
50
+ with engine.connect() as connection:
51
+ result = connection.execute(sqlalchemy.text("SELECT 1"))
52
+ try:
53
+ # 接続成功
54
+ if failed:
55
+ logger.info("DB接続成功")
56
+ break
57
+ finally:
58
+ result.close()
59
+ finally:
60
+ engine.dispose()
61
+ except Exception:
62
+ # 接続失敗
63
+ if not failed:
64
+ failed = True
65
+ logger.info(f"DB接続待機中 . . . (URL: {url})")
66
+ if time.time() - start_time >= timeout:
67
+ raise
68
+ time.sleep(1)
69
+
70
+
71
+ def safe_close(session: sqlalchemy.orm.Session):
72
+ """例外を出さずにセッションをクローズ。"""
73
+ try:
74
+ session.close()
75
+ except Exception:
76
+ pass
@@ -0,0 +1,24 @@
1
+ """tqdm用のユーティリティ集。"""
2
+
3
+ import logging
4
+
5
+ import tqdm
6
+
7
+
8
+ class TqdmStreamHandler(logging.StreamHandler):
9
+ """tqdm対応のStreamHandler。
10
+
11
+ 使用例::
12
+ import pytilpack.tqdm_
13
+
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format="[%(levelname)s] %(message)s",
17
+ handlers=[pytilpack.tqdm_.TqdmStreamHandler()],
18
+ )
19
+
20
+ """
21
+
22
+ def emit(self, record):
23
+ with tqdm.tqdm.external_write_mode(file=self.stream):
24
+ super().emit(record)