This commit is contained in:
@@ -11,13 +11,19 @@ jobs:
|
|||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
- name: Create virtualenv
|
||||||
|
run: |
|
||||||
|
python -m venv .venv
|
||||||
|
.venv/bin/pip install -r requirements.txt
|
||||||
|
- name: Run unit tests
|
||||||
|
run: .venv/bin/python -m unittest discover -s tests
|
||||||
- name: Execute build
|
- name: Execute build
|
||||||
|
run: |
|
||||||
|
.venv/bin/python -m build
|
||||||
|
- name: Publish artifacts
|
||||||
env:
|
env:
|
||||||
TWINE_REPOSITORY_URL: ${{ vars.PYPI_REGISTRY_URL }}
|
TWINE_REPOSITORY_URL: ${{ vars.PYPI_REGISTRY_URL }}
|
||||||
TWINE_USERNAME: ${{ vars.PUBLISHER_USERNAME }}
|
TWINE_USERNAME: ${{ vars.PUBLISHER_USERNAME }}
|
||||||
TWINE_PASSWORD: ${{ secrets.PUBLISHER_TOKEN }}
|
TWINE_PASSWORD: ${{ secrets.PUBLISHER_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
python -m venv .venv
|
.venv/bin/python -m twine upload --repository gitea dist/*{.whl,tar.gz}
|
||||||
.venv/bin/pip install -r requirements.txt
|
|
||||||
.venv/bin/python -m build
|
|
||||||
.venv/bin/python -m twine upload --repository gitea dist/*{.whl,tar.gz}
|
|
||||||
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
__pycache__
|
||||||
|
.venv
|
||||||
|
*.pyc
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pwo"
|
name = "pwo"
|
||||||
version = "0.0.1"
|
version = "0.0.2"
|
||||||
authors = [
|
authors = [
|
||||||
{ name="Walter Oggioni", email="oggioni.walter@gmail.com" },
|
{ name="Walter Oggioni", email="oggioni.walter@gmail.com" },
|
||||||
]
|
]
|
||||||
|
1
src/pwo/__init__.py
Normal file
1
src/pwo/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .private import format_filesize, async_retry, retry, async_test
|
82
src/pwo/private.py
Normal file
82
src/pwo/private.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import math
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Callable
|
||||||
|
from time import sleep
|
||||||
|
from asyncio import sleep as async_sleep, Runner
|
||||||
|
|
||||||
|
_size_uoms = ('B', 'KiB', 'MiB', 'GiB', 'KiB')
|
||||||
|
|
||||||
|
|
||||||
|
def format_filesize(size: int) -> str:
|
||||||
|
counter = 0
|
||||||
|
tmp_size = size
|
||||||
|
while tmp_size > 0:
|
||||||
|
tmp_size //= 1024
|
||||||
|
counter += 1
|
||||||
|
counter -= 1
|
||||||
|
return '%.2f ' % (size / math.pow(1024, counter)) + _size_uoms[counter]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionHandlerOutcome(Enum):
|
||||||
|
THROW = auto()
|
||||||
|
CONTINUE = auto()
|
||||||
|
|
||||||
|
|
||||||
|
def retry(
|
||||||
|
max_attempts: int = 3,
|
||||||
|
multiplier: float = 2,
|
||||||
|
initial_delay: float = 1.0,
|
||||||
|
exception_handler: Callable[[Exception], ExceptionHandlerOutcome] =
|
||||||
|
lambda _: ExceptionHandlerOutcome.CONTINUE
|
||||||
|
):
|
||||||
|
def wrapper(function):
|
||||||
|
def result(*args, **kwargs):
|
||||||
|
attempts = 0
|
||||||
|
delay = initial_delay
|
||||||
|
while True:
|
||||||
|
attempts += 1
|
||||||
|
try:
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
except Exception as ex:
|
||||||
|
if attempts < max_attempts and exception_handler(ex) == ExceptionHandlerOutcome.CONTINUE:
|
||||||
|
sleep(delay)
|
||||||
|
delay *= multiplier
|
||||||
|
else:
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def async_retry(
|
||||||
|
max_attempts: int = 3,
|
||||||
|
multiplier: float = 2,
|
||||||
|
initial_delay: float = 1.0,
|
||||||
|
exception_handler=lambda _: ExceptionHandlerOutcome.CONTINUE
|
||||||
|
):
|
||||||
|
def wrapper(function):
|
||||||
|
async def result(*args, **kwargs):
|
||||||
|
attempts = 0
|
||||||
|
delay = initial_delay
|
||||||
|
while True:
|
||||||
|
attempts += 1
|
||||||
|
try:
|
||||||
|
return await function(*args, **kwargs)
|
||||||
|
except Exception as ex:
|
||||||
|
if attempts < max_attempts and exception_handler(ex) == ExceptionHandlerOutcome.CONTINUE:
|
||||||
|
await async_sleep(delay)
|
||||||
|
delay *= multiplier
|
||||||
|
else:
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def async_test(coro):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
with Runner() as runner:
|
||||||
|
runner.run(coro(*args, **kwargs))
|
||||||
|
return wrapper
|
74
tests/test_private.py
Normal file
74
tests/test_private.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from src.pwo import retry, async_retry, async_test
|
||||||
|
|
||||||
|
|
||||||
|
class PrivateTest(unittest.TestCase):
|
||||||
|
def test_retry_until_success(self):
|
||||||
|
max_attempts = 20
|
||||||
|
attempt = 0
|
||||||
|
|
||||||
|
expected_result = object()
|
||||||
|
|
||||||
|
@retry(max_attempts=max_attempts, initial_delay=0)
|
||||||
|
def foo():
|
||||||
|
nonlocal attempt
|
||||||
|
attempt += 1
|
||||||
|
if attempt < 10:
|
||||||
|
raise Exception()
|
||||||
|
else:
|
||||||
|
return expected_result
|
||||||
|
|
||||||
|
self.assertEqual(expected_result, foo())
|
||||||
|
self.assertEqual(10, attempt)
|
||||||
|
|
||||||
|
def test_retry_until_max_attempt(self):
|
||||||
|
max_attempts = 20
|
||||||
|
attempt = 0
|
||||||
|
|
||||||
|
@retry(max_attempts=max_attempts, initial_delay=0)
|
||||||
|
def bar():
|
||||||
|
nonlocal attempt
|
||||||
|
attempt += 1
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
bar()
|
||||||
|
self.assertEqual(max_attempts, attempt)
|
||||||
|
|
||||||
|
@async_test
|
||||||
|
async def test_async_retry_until_success(self):
|
||||||
|
max_attempts = 20
|
||||||
|
attempt = 0
|
||||||
|
|
||||||
|
expected_result = object()
|
||||||
|
|
||||||
|
@async_retry(max_attempts=max_attempts, initial_delay=0)
|
||||||
|
async def foo():
|
||||||
|
nonlocal attempt
|
||||||
|
attempt += 1
|
||||||
|
if attempt < 10:
|
||||||
|
raise Exception()
|
||||||
|
else:
|
||||||
|
return expected_result
|
||||||
|
|
||||||
|
self.assertEqual(expected_result, await foo())
|
||||||
|
self.assertEqual(10, attempt)
|
||||||
|
|
||||||
|
@async_test
|
||||||
|
async def test_async_retry_until_max_attempt(self):
|
||||||
|
max_attempts = 20
|
||||||
|
attempt = 0
|
||||||
|
|
||||||
|
@async_retry(max_attempts=max_attempts, initial_delay=0)
|
||||||
|
async def bar():
|
||||||
|
nonlocal attempt
|
||||||
|
attempt += 1
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
await bar()
|
||||||
|
self.assertEqual(max_attempts, attempt)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Reference in New Issue
Block a user