Skip to content

Commit 7b0383d

Browse files
committed
🎉 init: first commit
0 parents  commit 7b0383d

File tree

8 files changed

+396
-0
lines changed

8 files changed

+396
-0
lines changed

.github/workflows/release.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- '*.*.*'
7+
8+
jobs:
9+
release:
10+
name: Release
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v3
15+
16+
- name: Set up Python 3.10
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: "3.10"
20+
21+
- name: Install Poetry
22+
uses: snok/install-poetry@v1
23+
with:
24+
virtualenvs-create: true
25+
virtualenvs-in-project: true
26+
installer-parallel: true
27+
28+
- name: Build project for distribution
29+
run: poetry build
30+
31+
- name: Check Version
32+
id: check-version
33+
run: |
34+
[[ "$(poetry version --short)" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
35+
|| echo ::set-output name=prerelease::true
36+
37+
- name: Create Release
38+
uses: ncipollo/release-action@v1
39+
with:
40+
artifacts: "dist/*"
41+
token: ${{ secrets.GITHUB_TOKEN }}
42+
draft: false
43+
prerelease: steps.check-version.outputs.prerelease == 'true'
44+
allowUpdates: true
45+
46+
- name: Publish to PyPI
47+
env:
48+
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
49+
run: poetry publish

.github/workflows/test.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: test suite
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
strategy:
11+
fail-fast: true
12+
matrix:
13+
os: [ "ubuntu-latest" ]
14+
python-version: [ "3.8", "3.9", "3.10" ]
15+
runs-on: ${{ matrix.os }}
16+
services:
17+
rabbitmq:
18+
image: redis:latest
19+
options: >-
20+
--health-cmd "redis-cli ping"
21+
--health-interval 10s
22+
--health-timeout 5s
23+
--health-retries 5
24+
ports:
25+
- 6379:6379
26+
steps:
27+
- uses: actions/checkout@v2
28+
- name: Set up Python ${{ matrix.python-version }}
29+
uses: actions/setup-python@v2
30+
with:
31+
python-version: ${{ matrix.python-version }}
32+
- name: Install Poetry
33+
uses: snok/install-poetry@v1
34+
with:
35+
virtualenvs-create: true
36+
virtualenvs-in-project: true
37+
installer-parallel: true
38+
- name: Load cached venv
39+
id: cached-poetry-dependencies
40+
uses: actions/cache@v2
41+
with:
42+
path: .venv
43+
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
44+
- name: Install dependencies
45+
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
46+
run: poetry install --no-interaction --no-root
47+
- name: Install library
48+
run: poetry install --no-interaction
49+
- name: Run tests
50+
env:
51+
REDIS_HOST: localhost
52+
run: |
53+
source .venv/bin/activate
54+
pytest tests/

.gitignore

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# created by virtualenv automatically
2+
# Byte-compiled / optimized / DLL files
3+
__pycache__/
4+
*.py[cod]
5+
*$py.class
6+
7+
# C extensions
8+
*.so
9+
10+
# Distribution / packaging
11+
.Python
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
wheels/
24+
pip-wheel-metadata/
25+
share/python-wheels/
26+
*.egg-info/
27+
.installed.cfg
28+
*.egg
29+
MANIFEST
30+
31+
# PyInstaller
32+
# Usually these files are written by a python script from a template
33+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
34+
*.manifest
35+
*.spec
36+
37+
# Installer logs
38+
pip-log.txt
39+
pip-delete-this-directory.txt
40+
41+
# Unit test / coverage reports
42+
htmlcov/
43+
.tox/
44+
.nox/
45+
.coverage
46+
.coverage.*
47+
.cache
48+
nosetests.xml
49+
coverage.xml
50+
*.cover
51+
*.py,cover
52+
.hypothesis/
53+
.pytest_cache/
54+
55+
# Translations
56+
*.mo
57+
*.pot
58+
59+
# Django stuff:
60+
*.log
61+
local_settings.py
62+
db.sqlite3
63+
db.sqlite3-journal
64+
65+
# Flask stuff:
66+
instance/
67+
.webassets-cache
68+
69+
# Scrapy stuff:
70+
.scrapy
71+
72+
# Sphinx documentation
73+
docs/_build/
74+
75+
# PyBuilder
76+
target/
77+
78+
# Jupyter Notebook
79+
.ipynb_checkpoints
80+
81+
# IPython
82+
profile_default/
83+
ipython_config.py
84+
85+
# pyenv
86+
.python-version
87+
88+
# pipenv
89+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
90+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
91+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
92+
# install all needed dependencies.
93+
#Pipfile.lock
94+
95+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
96+
__pypackages__/
97+
98+
# Celery stuff
99+
celerybeat-schedule
100+
celerybeat.pid
101+
102+
# SageMath parsed files
103+
*.sage.py
104+
105+
# Environments
106+
.env
107+
.venv
108+
env/
109+
venv/
110+
ENV/
111+
env.bak/
112+
venv.bak/
113+
114+
# Spyder project settings
115+
.spyderproject
116+
.spyproject
117+
118+
# Rope project settings
119+
.ropeproject
120+
121+
# mkdocs documentation
122+
/site
123+
124+
# mypy
125+
.mypy_cache/
126+
.dmypy.json
127+
dmypy.json
128+
129+
# Pyre type checker
130+
.pyre/
131+
132+
# pycharm
133+
.idea
134+
poetry.lock
135+
*.DS_Store
136+
137+
# docs/node_modules
138+
docs/node_modules

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# usepy-plugin-redis

example/demo.py

Whitespace-only changes.

pyproject.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[tool.poetry]
2+
name = "usepy-plugin-redis"
3+
version = "0.1.0"
4+
description = ""
5+
authors = ["miclon <jcnd@163.com>"]
6+
readme = "README.md"
7+
packages = [
8+
{ include = 'usepy_plugin_redis', from = 'src' }
9+
]
10+
11+
[tool.poetry.dependencies]
12+
python = "^3.8"
13+
redis = "^4.6.0"
14+
15+
[tool.poetry.group.test.dependencies]
16+
pytest = [
17+
{ version = "^7.0.0", python = ">=3.6,<3.7" },
18+
{ version = "^7.3.1", python = ">=3.7" }
19+
]
20+
21+
[build-system]
22+
requires = ["poetry-core"]
23+
build-backend = "poetry.core.masonry.api"
24+

src/usepy_plugin_redis/__init__.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import logging
2+
3+
import redis
4+
import time
5+
from threading import local
6+
7+
MAX_SEND_ATTEMPTS = 6 # 最大发送重试次数
8+
MAX_CONNECTION_ATTEMPTS = float('inf') # 最大连接重试次数
9+
MAX_CONNECTION_DELAY = 2 ** 5 # 最大延迟时间
10+
11+
logger = logging.Logger(__name__)
12+
13+
14+
class RedisStore:
15+
16+
def __init__(self, *, host=None, port=None, password=None, **kwargs):
17+
"""
18+
:param host: Redis host
19+
:param port: Redis port
20+
:param password: Redis password
21+
:param kwargs: Redis parameters
22+
"""
23+
self.state = local()
24+
self.parameters = {
25+
'host': host or 'localhost',
26+
'port': port or 6379,
27+
'password': password or None,
28+
}
29+
if kwargs:
30+
self.parameters.update(kwargs)
31+
32+
def _create_connection(self):
33+
attempts = 1
34+
delay = 1
35+
while attempts <= MAX_CONNECTION_ATTEMPTS:
36+
try:
37+
connector = redis.Redis(**self.parameters)
38+
if attempts > 1:
39+
logger.warning(f"RedisStore connection succeeded after {attempts} attempts", )
40+
connector.ping()
41+
return connector
42+
except redis.ConnectionError as exc:
43+
logger.warning(f"RedisStore connection error<{exc}>; retrying in {delay} seconds")
44+
attempts += 1
45+
time.sleep(delay)
46+
if delay < MAX_CONNECTION_DELAY:
47+
delay *= 2
48+
delay = min(delay, MAX_CONNECTION_DELAY)
49+
raise redis.ConnectionError("RedisStore connection error, max attempts reached")
50+
51+
@property
52+
def connection(self):
53+
connection = getattr(self.state, "connection", None)
54+
if connection is None:
55+
connection = self.state.connection = self._create_connection()
56+
return connection
57+
58+
@connection.deleter
59+
def connection(self):
60+
if _connection := getattr(self.state, "connection", None):
61+
try:
62+
_connection.close()
63+
except Exception as exc:
64+
logger.exception(f"RedisStore connection close error<{exc}>")
65+
del self.state.connection
66+
67+
68+
class RedisStreamMixin:
69+
connection: redis.Redis
70+
71+
def _create_group(self, stream, group):
72+
try:
73+
self.connection.xgroup_create(stream, group, id='0', mkstream=True)
74+
except redis.exceptions.ResponseError as e:
75+
if "already exists" not in str(e):
76+
raise e
77+
78+
def send(self, stream, message, **kwargs):
79+
"""发送消息"""
80+
attempts = 1
81+
while True:
82+
try:
83+
self.connection.xadd(stream, message, **kwargs)
84+
return message
85+
except Exception as exc:
86+
del self.connection
87+
attempts += 1
88+
if attempts > MAX_SEND_ATTEMPTS:
89+
raise exc
90+
91+
def start_consuming(self, stream, group, consumer, callback, prefetch=1, **kwargs):
92+
"""开始消费"""
93+
self._create_group(stream, group)
94+
while True:
95+
try:
96+
messages = self.connection.xreadgroup(group, consumer, {stream: '>'}, count=prefetch, **kwargs)
97+
for message in messages:
98+
_, msg = message
99+
callback(msg)
100+
except redis.ConnectionError:
101+
logger.warning("RedisStore consume connection error, reconnecting...")
102+
del self.connection
103+
time.sleep(1)
104+
except Exception as e:
105+
logger.exception(f"RedisStore consume error<{e}>, reconnecting...")
106+
del self.connection
107+
time.sleep(1)
108+
109+
110+
class RedisStreamStore(RedisStore, RedisStreamMixin):
111+
pass
112+
113+
114+
useRedis = RedisStore
115+
useRedisStreamStore = RedisStreamStore

0 commit comments

Comments
 (0)