voyolib 0.0.1__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.
- voyolib-0.0.1/.github/workflows/release.yml +35 -0
- voyolib-0.0.1/.gitignore +39 -0
- voyolib-0.0.1/.python-version +1 -0
- voyolib-0.0.1/PKG-INFO +5 -0
- voyolib-0.0.1/README.md +0 -0
- voyolib-0.0.1/main.py +6 -0
- voyolib-0.0.1/pyproject.toml +14 -0
- voyolib-0.0.1/voyo/db/1 +0 -0
- voyolib-0.0.1/voyo/db/mysql/conn_pool.py +146 -0
- voyolib-0.0.1/voyo/db/mysql/transaction.py +59 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
release:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
id-token: write
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.13"
|
|
24
|
+
|
|
25
|
+
- name: Install build dependencies
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
pip install build
|
|
29
|
+
|
|
30
|
+
- name: Build package
|
|
31
|
+
run: python -m build
|
|
32
|
+
|
|
33
|
+
- name: Publish to PyPI
|
|
34
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
35
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
voyolib-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
|
|
14
|
+
# Environment variables
|
|
15
|
+
.env
|
|
16
|
+
.env.*
|
|
17
|
+
|
|
18
|
+
# Test / coverage
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
htmlcov/
|
|
24
|
+
.tox/
|
|
25
|
+
noxfile.py
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
*.swp
|
|
31
|
+
*.swo
|
|
32
|
+
*~
|
|
33
|
+
|
|
34
|
+
# OS
|
|
35
|
+
.DS_Store
|
|
36
|
+
Thumbs.db
|
|
37
|
+
|
|
38
|
+
# Logs
|
|
39
|
+
*.log
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
voyolib-0.0.1/PKG-INFO
ADDED
voyolib-0.0.1/README.md
ADDED
|
File without changes
|
voyolib-0.0.1/main.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "voyolib"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Python library for Voyo"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[build-system]
|
|
10
|
+
requires = ["hatchling"]
|
|
11
|
+
build-backend = "hatchling.build"
|
|
12
|
+
|
|
13
|
+
[tool.hatch.build.targets.wheel]
|
|
14
|
+
packages = ["voyo"]
|
voyolib-0.0.1/voyo/db/1
ADDED
|
File without changes
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import queue
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import pymysql
|
|
8
|
+
|
|
9
|
+
_default_pool: Optional["YoConnPool"] = None
|
|
10
|
+
|
|
11
|
+
def set_default_pool(pool: "YoConnPool"):
|
|
12
|
+
global _default_pool
|
|
13
|
+
_default_pool = pool
|
|
14
|
+
|
|
15
|
+
def get_default_pool() -> Optional["YoConnPool"]:
|
|
16
|
+
return _default_pool
|
|
17
|
+
|
|
18
|
+
class ConnPool(ABC):
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def get_conn(self):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def release_conn(self, conn):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
class PooledConnection:
|
|
29
|
+
|
|
30
|
+
def __init__(self, raw_conn, created_at: float, is_burst: bool = False):
|
|
31
|
+
self.raw = raw_conn
|
|
32
|
+
self.created_at = created_at
|
|
33
|
+
self.is_burst = is_burst
|
|
34
|
+
|
|
35
|
+
def is_expired(self, max_timeout: float) -> bool:
|
|
36
|
+
return (time.time() - self.created_at) > max_timeout
|
|
37
|
+
|
|
38
|
+
def close(self):
|
|
39
|
+
if self.raw:
|
|
40
|
+
try:
|
|
41
|
+
self.raw.close()
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
self.raw = None
|
|
45
|
+
|
|
46
|
+
class YoConnPool(ConnPool):
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
host: str = "localhost",
|
|
52
|
+
port: int = 3306,
|
|
53
|
+
user: str = "",
|
|
54
|
+
password: str = "",
|
|
55
|
+
database: str = "",
|
|
56
|
+
charset: str = "utf8mb4",
|
|
57
|
+
pool_size: int = 5,
|
|
58
|
+
max_burst: int = 10,
|
|
59
|
+
max_timeout: float = 600,
|
|
60
|
+
):
|
|
61
|
+
self._config = {
|
|
62
|
+
"host": host,
|
|
63
|
+
"port": port,
|
|
64
|
+
"user": user,
|
|
65
|
+
"password": password,
|
|
66
|
+
"database": database,
|
|
67
|
+
"charset": charset,
|
|
68
|
+
"autocommit": False,
|
|
69
|
+
}
|
|
70
|
+
self._pool_size = pool_size
|
|
71
|
+
self._max_burst = max_burst
|
|
72
|
+
self._max_timeout = max_timeout
|
|
73
|
+
self._pool = queue.Queue(maxsize=pool_size)
|
|
74
|
+
self._lock = threading.Lock()
|
|
75
|
+
self._burst_lock = threading.Lock()
|
|
76
|
+
self._burst_count = 0
|
|
77
|
+
self._conn_map = {}
|
|
78
|
+
|
|
79
|
+
for _ in range(pool_size):
|
|
80
|
+
pooled = self._create_connection()
|
|
81
|
+
self._pool.put(pooled)
|
|
82
|
+
|
|
83
|
+
def _create_connection(self, is_burst: bool = False) -> PooledConnection:
|
|
84
|
+
raw = pymysql.connect(**self._config)
|
|
85
|
+
return PooledConnection(raw, time.time(), is_burst=is_burst)
|
|
86
|
+
|
|
87
|
+
def _recycle(self, pooled: PooledConnection) -> PooledConnection:
|
|
88
|
+
pooled.close()
|
|
89
|
+
return self._create_connection()
|
|
90
|
+
|
|
91
|
+
def get_conn(self):
|
|
92
|
+
is_burst = False
|
|
93
|
+
try:
|
|
94
|
+
pooled = self._pool.get(block=False)
|
|
95
|
+
except queue.Empty:
|
|
96
|
+
|
|
97
|
+
with self._burst_lock:
|
|
98
|
+
if self._burst_count < self._max_burst:
|
|
99
|
+
self._burst_count += 1
|
|
100
|
+
is_burst = True
|
|
101
|
+
|
|
102
|
+
pooled = self._create_connection(is_burst=True)
|
|
103
|
+
|
|
104
|
+
if not pooled.is_burst and pooled.is_expired(self._max_timeout):
|
|
105
|
+
pooled = self._recycle(pooled)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
pooled.raw.ping()
|
|
109
|
+
except Exception:
|
|
110
|
+
pooled = self._recycle(pooled)
|
|
111
|
+
|
|
112
|
+
with self._lock:
|
|
113
|
+
self._conn_map[pooled.raw] = pooled
|
|
114
|
+
return pooled.raw
|
|
115
|
+
|
|
116
|
+
def release_conn(self, conn):
|
|
117
|
+
with self._lock:
|
|
118
|
+
pooled = self._conn_map.pop(conn, None)
|
|
119
|
+
|
|
120
|
+
if pooled is None:
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
conn.close()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if pooled.is_burst:
|
|
129
|
+
try:
|
|
130
|
+
conn.rollback()
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
pooled.close()
|
|
134
|
+
with self._burst_lock:
|
|
135
|
+
self._burst_count = max(0, self._burst_count - 1)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
conn.rollback()
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
self._pool.put(pooled, block=False)
|
|
145
|
+
except queue.Full:
|
|
146
|
+
pooled.close()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from voyo.db.mysql.conn_pool import YoConnPool, get_default_pool
|
|
6
|
+
|
|
7
|
+
_tx_local = threading.local()
|
|
8
|
+
|
|
9
|
+
def _get_tx_stack():
|
|
10
|
+
if not hasattr(_tx_local, "stack"):
|
|
11
|
+
_tx_local.stack = []
|
|
12
|
+
return _tx_local.stack
|
|
13
|
+
|
|
14
|
+
class Transaction:
|
|
15
|
+
|
|
16
|
+
def __init__(self, propagation="required", pool=None):
|
|
17
|
+
if propagation not in ("required", "new"):
|
|
18
|
+
raise ValueError("propagation must be 'required' or 'new'")
|
|
19
|
+
self.propagation = propagation
|
|
20
|
+
self.pool = pool or get_default_pool()
|
|
21
|
+
if self.pool is None:
|
|
22
|
+
raise RuntimeError(
|
|
23
|
+
"No connection pool configured for @Transaction. "
|
|
24
|
+
"Call yo_mysql.set_default_pool(pool) first."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def __call__(self, func):
|
|
28
|
+
sig = inspect.signature(func)
|
|
29
|
+
params = list(sig.parameters.values())
|
|
30
|
+
|
|
31
|
+
has_conn_param = bool(params) and params[-1].name == 'conn'
|
|
32
|
+
|
|
33
|
+
@functools.wraps(func)
|
|
34
|
+
def wrapper(*args, **kwargs):
|
|
35
|
+
stack = _get_tx_stack()
|
|
36
|
+
|
|
37
|
+
if stack and self.propagation != "new":
|
|
38
|
+
conn = stack[-1]["conn"]
|
|
39
|
+
|
|
40
|
+
if has_conn_param and "conn" not in kwargs:
|
|
41
|
+
kwargs["conn"] = conn
|
|
42
|
+
|
|
43
|
+
return func(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
conn = self.pool.get_conn()
|
|
46
|
+
stack.append({"conn": conn})
|
|
47
|
+
|
|
48
|
+
if has_conn_param and "conn" not in kwargs:
|
|
49
|
+
kwargs["conn"] = conn
|
|
50
|
+
try:
|
|
51
|
+
result = func(*args, **kwargs)
|
|
52
|
+
conn.commit()
|
|
53
|
+
return result
|
|
54
|
+
finally:
|
|
55
|
+
conn.rollback()
|
|
56
|
+
stack.pop()
|
|
57
|
+
self.pool.release_conn(conn)
|
|
58
|
+
|
|
59
|
+
return wrapper
|