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.
- pytilpack-0.1.0/LICENSE +21 -0
- pytilpack-0.1.0/PKG-INFO +42 -0
- pytilpack-0.1.0/README.md +13 -0
- pytilpack-0.1.0/pyproject.toml +80 -0
- pytilpack-0.1.0/pytilpack/__init__.py +0 -0
- pytilpack-0.1.0/pytilpack/flask_.py +61 -0
- pytilpack-0.1.0/pytilpack/openai_.py +121 -0
- pytilpack-0.1.0/pytilpack/python_.py +28 -0
- pytilpack-0.1.0/pytilpack/sqlalchemy_.py +76 -0
- pytilpack-0.1.0/pytilpack/tqdm_.py +24 -0
pytilpack-0.1.0/LICENSE
ADDED
|
@@ -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.
|
pytilpack-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/psf/black)
|
|
32
|
+
[](https://github.com/ak110/pytilpack/actions/workflows/python-app.yml)
|
|
33
|
+
[](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
|
+
[](https://github.com/psf/black)
|
|
4
|
+
[](https://github.com/ak110/pytilpack/actions/workflows/python-app.yml)
|
|
5
|
+
[](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)
|