Skip to content

Commit

Permalink
WIP: Work in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
sylvainmouquet committed Oct 2, 2024
1 parent 1485d03 commit 8c6f9d1
Show file tree
Hide file tree
Showing 11 changed files with 451 additions and 3 deletions.
53 changes: 53 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Release

on:
push:
tags:
- '*'

jobs:
release:
runs-on: ubuntu-latest
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
- name: Extract version from tag
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Install uv
uses: astral-sh/setup-uv@v2
- name: Use uv with Python version
run: uv venv --python 3.12
- run: make install
- run: make build
- run: make test
- name: mint API token
id: mint-token
run: |
# retrieve the ambient OIDC token
resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi")
oidc_token=$(jq -r '.value' <<< "${resp}")
# exchange the OIDC token for an API token
resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}")
api_token=$(jq -r '.token' <<< "${resp}")
# mask the newly minted API token, so that we don't accidentally leak it
echo "::add-mask::${api_token}"
# see the next step in the workflow for an example of using this step output
echo "api-token=${api_token}" >> "${GITHUB_OUTPUT}"
- name: publish
# gh-action-pypi-publish uses TWINE_PASSWORD automatically
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ steps.mint-token.outputs.api-token }}

37 changes: 37 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Run Tests

on:
push:
branches:
- '*'
pull_request:
branches:
- '*'
schedule:
- cron: '0 9 * * 0' # At 09:00 on Sunday
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v2
- name: Use uv with Python version
run: uv venv --python ${{ matrix.python-version }}
- name: Install dependencies
run: make install
- name: Build
run: VERSION=0.0.1 make build
- name: Run tests
run: make test
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ __pycache__/
*.py[cod]
*$py.class

/.idea/*

# C extensions
*.so

Expand Down Expand Up @@ -160,3 +162,7 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/


# macOS system files
.DS_Store
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Sylvain MOUQUET
Copyright (c) 2024 Concurrency-Limiter

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
96 changes: 96 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
SHELL:=/bin/bash

SUPPORTED_COMMANDS := test
SUPPORTS_MAKE_ARGS := $(findstring $(firstword $(MAKECMDGOALS)), $(SUPPORTED_COMMANDS))
ifneq "$(SUPPORTS_MAKE_ARGS)" ""
COMMAND_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
COMMAND_ARGS := $(subst :,\:,$(COMMAND_ARGS))
$(eval $(COMMAND_ARGS):;@:)
endif

# Git workflow commands
.PHONY: wip
wip:
git add .
git commit -m "WIP: Work in progress"
git push

# Install command
.PHONY: install
install:
uv sync --all-extras --dev

# Build command
.PHONY: build
build: check-version
rm -rf dist/* || true
# ls -al
./scripts/version.sh "${VERSION}"
@cat pyproject.toml | grep version
@cat reattempt/__init__.py | grep version
uv build

.PHONY: check-version
check-version:
@if [ -z "${VERSION}" ]; then \
echo "VERSION is not set. Please set the VERSION environment variable."; \
exit 1; \
fi

# Deploy command
.PHONY: deploy
deploy:
uvx twine upload dist/*

# Install local build command
.PHONY: install-local
install-local:
pip3 install dist/*.whl

# Test command
.PHONY: test
test:
@echo "Modified arguments: $(new_args)"
@if [ -z "$(COMMAND_ARGS)" ]; then \
uv run pytest -v --log-cli-level=INFO; \
else \
uv run pytest -v --log-cli-level=INFO $(new_args); \
fi

# Lint command
.PHONY: lint
lint:
uv run ruff check --fix
uv run ruff format
uv run ruff format --check

# Update dependencies
.PHONY: update
update:
uv sync

# Check for outdated dependencies
.PHONY: check-deps
check-deps:
.venv/bin/pip list --outdated

# Run type checking
.PHONY: type-check
type-check:
PYRIGHT_PYTHON_FORCE_VERSION=latest uv run pyright

# Display all available commands
.PHONY: help
help:
@echo "Available commands:"
@echo " wip - Commit and push work in progress"
@echo " install - Install dependencies"
@echo " build - Build the project"
@echo " deploy - Deploy the project"
@echo " install-local - Install the build locally"
@echo " test - Run tests"
@echo " lint - Run linter"
@echo " update - Update dependencies"
@echo " check-deps - Check for outdated dependencies"
@echo " type-check - Run type checking"
@echo " help - Display this help message"
142 changes: 140 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,140 @@
# concurrency-limiter
Python Library
# Concurrency-Limiter

Concurrency-Limiter is a python decorator to limit the number of concurrent executor in asyncio.gather

### Demonstration:

```python
from reattempt import reattempt

@reattempt(max_retries=5, min_time=0.1, max_time=2)
def simulate_network_failure():
raise Exception("Connection timeout")

if __name__ == "__main__":
simulate_network_failure()

------------------------------------------------------- live log call -------------------------------------------------------
WARNING root:__init__.py:167 [RETRY] Attempt 1/5 failed, retrying in 0.17 seconds...
WARNING root:__init__.py:167 [RETRY] Attempt 2/5 failed, retrying in 0.19 seconds...
WARNING root:__init__.py:167 [RETRY] Attempt 3/5 failed, retrying in 0.19 seconds...
WARNING root:__init__.py:167 [RETRY] Attempt 4/5 failed, retrying in 0.19 seconds...
WARNING root:__init__.py:163 [RETRY] Attempt 5/5 failed, stopping
ERROR root:__init__.py:177 [RETRY] Max retries reached
```

## Table of Contents

- [ReAttempt](#ReAttempt)
- [Table of Contents](#table-of-contents)
- [Description](#description)
- [Installation](#installation)
- [Usage](#usage)
- [License](#license)
- [Contact](#contact)

## Description

ReAttempt is a Python library that provides a decorator to automatically retry a function when exceptions are raised. It uses an exponential backoff strategy to wait between retries, ensuring that the function has multiple chances to succeed before ultimately failing.

## Installation

```bash
# Install the dependency
pip install reattempt
uv add reattempt
poetry add reattempt
```

## Usage

```python
from reattempt import reattempt
import asyncio
import random

# List of flowers for our examples
flowers = ["Rose", "Tulip", "Sunflower", "Daisy", "Lily"]

# Synchronous function example
@reattempt
def plant_flower():
flower = random.choice(flowers)
print(f"Attempting to plant a {flower}")
if random.random() < 0.8: # 80% chance of failure
raise Exception(f"The {flower} didn't take root")
return f"{flower} planted successfully"

# Synchronous generator example
@reattempt
def grow_flowers():
for _ in range(3):
flower = random.choice(flowers)
print(f"Growing {flower}")
yield flower
if random.random() < 0.5: # 50% chance of failure at the end
raise Exception("The garden needs more fertilizer")

# Asynchronous function example
@reattempt
async def water_flower():
flower = random.choice(flowers)
print(f"Watering the {flower}")
await asyncio.sleep(0.1) # Simulating watering time
if random.random() < 0.6: # 60% chance of failure
raise Exception(f"The {flower} needs more water")
return f"{flower} is well-watered"

# Asynchronous generator function example
@reattempt
async def harvest_flowers():
for _ in range(3):
flower = random.choice(flowers)
print(f"Harvesting {flower}")
yield flower
await asyncio.sleep(0.1) # Time between harvests
if random.random() < 0.4: # 40% chance of failure at the end
raise Exception("The garden needs more care")

async def tend_garden():
# Plant a flower (sync function)
try:
result = plant_flower()
print(result)
except Exception as e:
print(f"Planting error: {e}")

# Grow flowers (sync generator)
try:
for flower in grow_flowers():
print(f"Grown: {flower}")
except Exception as e:
print(f"Growing error: {e}")

# Water a flower (async function)
try:
result = await water_flower()
print(result)
except Exception as e:
print(f"Watering error: {e}")

# Harvest flowers (async generator function)
try:
async for flower in harvest_flowers():
print(f"Harvested: {flower}")
except Exception as e:
print(f"Harvesting error: {e}")

if __name__ == "__main__":
asyncio.run(tend_garden())
```


## License

ReAttempt is released under the MIT License. See the [LICENSE](LICENSE) file for more details.

## Contact

For questions, suggestions, or issues related to ReAttempt, please open an issue on the GitHub repository.

Loading

0 comments on commit 8c6f9d1

Please sign in to comment.