Skip to content

Commit

Permalink
Merge branch 'release/2.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
fabfuel committed May 15, 2023
2 parents 89f4eba + 44692ad commit e924f71
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 226 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-beta.3"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v2
Expand Down
8 changes: 8 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ This is the simplest example. Just decorate a function with the ``@circuit`` dec
def external_call():
...

Async functions are also supported::

@circuit
async def external_call():
...

This decorator sets up a circuit breaker with the default settings. The circuit breaker:

Expand Down Expand Up @@ -129,6 +134,9 @@ By default, the circuit breaker will raise a ``CircuitBreaker`` exception when t
You can instead specify a function to be called when the circuit is opened. This function can be specified with the
``fallback_function`` parameter and will be called with the same parameters as the decorated function would be.

The fallback type of call must also match the decorated function. For instance, if the decorated function is an
async generator, the ``fallback_function`` must be an async generator as well.

Advanced Usage
--------------
If you apply circuit breakers to a couple of functions and you always set specific options other than the default values,
Expand Down
104 changes: 68 additions & 36 deletions circuitbreaker.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import

from functools import wraps
from asyncio import iscoroutinefunction
from datetime import datetime, timedelta
from inspect import isgeneratorfunction, isclass
from typing import AnyStr, Iterable
from functools import wraps
from inspect import isgeneratorfunction, isasyncgenfunction, isclass
from math import ceil, floor
from time import monotonic
from typing import AnyStr, Iterable

try:
from time import monotonic
except ImportError:
from monotonic import monotonic

# Python2 vs Python3 strings
try:
STRING_TYPES = (basestring,)
except NameError:
STRING_TYPES = (bytes, str)

STRING_TYPES = (bytes, str)
STATE_CLOSED = 'closed'
STATE_OPEN = 'open'
STATE_HALF_OPEN = 'half_open'
Expand Down Expand Up @@ -145,20 +131,52 @@ def decorate(self, function):

CircuitBreakerMonitor.register(self)

if isgeneratorfunction(function):
call = self.call_generator
else:
call = self.call
if iscoroutinefunction(function) or isasyncgenfunction(function):
return self._decorate_async(function)

return self._decorate_sync(function)

def _decorate_sync(self, function):
@wraps(function)
def wrapper(*args, **kwargs):
if self.opened:
if self.fallback_function:
return self.fallback_function(*args, **kwargs)
raise CircuitBreakerError(self)
return call(function, *args, **kwargs)
return self.call(function, *args, **kwargs)

return wrapper
@wraps(function)
def gen_wrapper(*args, **kwargs):
if self.opened:
if self.fallback_function:
yield from self.fallback_function(*args, **kwargs)
return
raise CircuitBreakerError(self)
yield from self.call_generator(function, *args, **kwargs)

return gen_wrapper if isgeneratorfunction(function) else wrapper

def _decorate_async(self, function):
@wraps(function)
async def awrapper(*args, **kwargs):
if self.opened:
if self.fallback_function:
return await self.fallback_function(*args, **kwargs)
raise CircuitBreakerError(self)
return await self.call_async(function, *args, **kwargs)

@wraps(function)
async def gen_awrapper(*args, **kwargs):
if self.opened:
if self.fallback_function:
async for el in self.fallback_function(*args, **kwargs):
yield el
return
raise CircuitBreakerError(self)
async for el in self.call_async_generator(function, *args, **kwargs):
yield el

return gen_awrapper if isasyncgenfunction(function) else awrapper

def call(self, func, *args, **kwargs):
"""
Expand All @@ -179,6 +197,25 @@ def call_generator(self, func, *args, **kwargs):
for el in func(*args, **kwargs):
yield el

async def call_async(self, func, *args, **kwargs):
"""
Calls the decorated async function and applies the circuit breaker
rules on success or failure
:param func: Decorated async function
"""
with self:
return await func(*args, **kwargs)

async def call_async_generator(self, func, *args, **kwargs):
"""
Calls the decorated async generator function and applies the circuit breaker
rules on success or failure
:param func: Decorated async generator function
"""
with self:
async for el in func(*args, **kwargs):
yield el

def __call_succeeded(self):
"""
Close circuit after successful execution and reset failure count
Expand Down Expand Up @@ -276,30 +313,25 @@ def register(cls, circuit_breaker):
cls.circuit_breakers[circuit_breaker.name] = circuit_breaker

@classmethod
def all_closed(cls):
# type: () -> bool
def all_closed(cls) -> bool:
return len(list(cls.get_open())) == 0

@classmethod
def get_circuits(cls):
# type: () -> Iterable[CircuitBreaker]
def get_circuits(cls) -> Iterable[CircuitBreaker]:
return cls.circuit_breakers.values()

@classmethod
def get(cls, name):
# type: (AnyStr) -> CircuitBreaker
def get(cls, name: AnyStr) -> CircuitBreaker:
return cls.circuit_breakers.get(name)

@classmethod
def get_open(cls):
# type: () -> Iterable[CircuitBreaker]
def get_open(cls) -> Iterable[CircuitBreaker]:
for circuit in cls.get_circuits():
if circuit.opened:
yield circuit

@classmethod
def get_closed(cls):
# type: () -> Iterable[CircuitBreaker]
def get_closed(cls) -> Iterable[CircuitBreaker]:
for circuit in cls.get_circuits():
if circuit.closed:
yield circuit
Expand Down
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
asyncio_mode = auto
2 changes: 0 additions & 2 deletions requirements.txt

This file was deleted.

4 changes: 2 additions & 2 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mock; python_version < '3.3'
pytest
pytest-asyncio
pytest-mock
pytest-cov
coverage
flake8
flake8
9 changes: 3 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ def readme():
return f.read()


dependencies = ["typing; python_version < '3.5'", "monotonic; python_version < '3.0'"]

setup(
name='circuitbreaker',
version='1.4.0',
version='2.0.0',
url='https://github.com/fabfuel/circuitbreaker',
download_url='https://github.com/fabfuel/circuitbreaker/archive/1.3.1.tar.gz',
license='BSD-3-Clause',
Expand All @@ -25,15 +23,14 @@ def readme():
include_package_data=True,
zip_safe=False,
platforms='any',
install_requires=dependencies,
classifiers=[
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: POSIX',
'Operating System :: MacOS',
'Operating System :: Unix',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
]
],
options={'bdist_wheel': {'universal': True}}
)
153 changes: 153 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import asyncio
import time
from enum import Enum

import pytest

from circuitbreaker import CircuitBreakerMonitor


class FunctionType(str, Enum):
sync_function = "sync-function"
sync_generator = "sync-generator"
async_function = "async-function"
async_generator = "async-generator"


@pytest.fixture(autouse=True)
def clean_circuit_breaker_monitor():
CircuitBreakerMonitor.circuit_breakers = {}


@pytest.fixture(params=FunctionType, ids=[e.value for e in FunctionType])
def function_type(request):
return request.param


@pytest.fixture
def is_async(function_type):
return function_type.startswith("async-")


@pytest.fixture
def is_generator(function_type):
return function_type.endswith("-generator")


@pytest.fixture
def function_factory(function_type):
def factory(inner_call):
def _sync(*a, **kwa):
return inner_call(*a, **kwa)

def _sync_gen(*a, **kwa):
yield inner_call(*a, **kwa)

async def _async(*a, **kwa):
return inner_call(*a, **kwa)

async def _async_gen(*a, **kwa):
yield inner_call(*a, **kwa)

mapping = {
FunctionType.sync_function: _sync,
FunctionType.sync_generator: _sync_gen,
FunctionType.async_function: _async,
FunctionType.async_generator: _async_gen,
}
return mapping[function_type]

return factory


@pytest.fixture
def resolve_call(function_type):
"""
This fixture helps abstract calls from other fixtures that have sync and
async, function and generator versions.
For example, this:
if is_async:
if is_generator:
result = [el async for el in function()]
else:
result = await function()
else:
if is_generator:
result = list(function())
else:
result = function()
Can be replaced with:
result = await resolve_call(function())
"""
async def _sync(value):
return value

async def _sync_gen(generator):
return list(generator)

async def _async(coroutine):
return await coroutine

async def _async_gen(async_generator):
return [el async for el in async_generator]

mapping = {
FunctionType.sync_function: _sync,
FunctionType.sync_generator: _sync_gen,
FunctionType.async_function: _async,
FunctionType.async_generator: _async_gen,
}
return mapping[function_type]


@pytest.fixture
def sleep(is_async):
async def _sleep(secs):
if is_async:
await asyncio.sleep(secs)
else:
time.sleep(secs)

return _sleep


@pytest.fixture
def mock_function_call(mocker):
return mocker.Mock(return_value=object())


@pytest.fixture
def mock_fallback_call(mocker):
return mocker.Mock(return_value=object())


@pytest.fixture
def function_call_return_value(is_generator, mock_function_call):
value = mock_function_call.return_value
return [value] if is_generator else value


@pytest.fixture
def fallback_call_return_value(is_generator, mock_fallback_call):
value = mock_fallback_call.return_value
return [value] if is_generator else value


@pytest.fixture
def function_call_error(mock_function_call):
error = IOError
mock_function_call.side_effect = error
return error


@pytest.fixture
def function(function_factory, mock_function_call):
return function_factory(mock_function_call)


@pytest.fixture
def fallback_function(function_factory, mock_fallback_call):
return function_factory(mock_fallback_call)
Loading

0 comments on commit e924f71

Please sign in to comment.