Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.env
.coverage
.gitignore
.idea
.mypy_cache
.ruff_cache
.vscode
.git
.pytest_cache
.DS_Store
*.yml
Dockerfile
**/__pycache__
.hypothesis
.venv
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: main

on:
push:
branches:
- main
pull_request: {}

concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v2
- uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
- run: uv python install 3.13
- run: just install lint-ci

pytest:
runs-on: ubuntu-latest
services:
redis:
image: redis:8
ports:
- 6379:6379
# Set health checks to wait until redis has started
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: astral-sh/setup-uv@v3
- run: uv python install 3.13
- run: |
uv sync --all-extras --no-install-project
uv run --no-sync pytest . --cov=. --cov-report xml
env:
PYTHONDONTWRITEBYTECODE: 1
PYTHONUNBUFFERED: 1
REDIS_URL: redis://127.0.0.1:6379/0
17 changes: 17 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Publish Package

on:
release:
types:
- published

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v2
- uses: astral-sh/setup-uv@v3
- run: just publish
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generic things
*.pyc
*~
__pycache__/*
*.swp
*.sqlite3
*.map
.vscode
.idea
.DS_Store
.env
.mypy_cache
.pytest_cache
.ruff_cache
.coverage
htmlcov/
coverage.xml
pytest.xml
dist/
.python-version
.venv
/*.egg-info/
.zed
193 changes: 193 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Project Context for Agents

## Project Overview

This is a Python library called `redis-timers` that provides a framework for managing timed events using Redis as the backend. The library allows developers to schedule timers that trigger handlers at specific times, with payloads that are automatically validated using Pydantic schemas.

### Key Components

1. **Timers** - Main class that manages timer scheduling and execution
2. **Router** - Registration system for timer handlers
3. **Handlers** - Functions that are triggered when timers expire
4. **Locking System** - Ensures timers are processed only once even in distributed environments
5. **Settings** - Configuration for Redis keys and separators

### Technologies Used

- **Python 3.13** - Primary programming language
- **Redis** - Backend storage for timer data
- **Pydantic** - Data validation for timer payloads
- **AsyncIO** - Asynchronous programming model
- **Docker** - Containerization for development and deployment
- **uv** - Python package manager and project management
- **Ruff** - Code linting and formatting
- **MyPy** - Static type checking
- **Pytest** - Testing framework

## Project Structure

```
redis-timers/
├── redis_timers/ # Main library code
│ ├── __init__.py # Package initialization
│ ├── handler.py # Timer handler definitions
│ ├── lock.py # Redis-based locking mechanisms
│ ├── router.py # Router for registering handlers
│ ├── settings.py # Configuration settings
│ ├── timers.py # Core timer functionality
│ └── py.typed # Type checking marker
├── tests/ # Unit tests
├── Dockerfile # Container definition
├── docker-compose.yml # Development environment setup
├── Justfile # Task runner commands
├── pyproject.toml # Project configuration
├── poetry.lock # Dependency lock file
└── uv.lock # Alternative dependency lock file
```

## Building and Running

### Development Environment Setup

1. Install dependencies:
```bash
just install
```

2. Run tests:
```bash
just test
```

3. Lint and format code:
```bash
just lint
```

4. Build Docker image:
```bash
just build
```

### Running the Application

The library is designed to be used as a dependency in other projects. To use it:

1. Import the necessary components:
```python
from redis_timers import Timers, Router
```

2. Create routers and register handlers:
```python
router = Router()

@router.handler(schema=MySchema)
async def my_timer_handler(data: MySchema):
# Handle timer event
pass
```

3. Initialize timers with a Redis client:
```python
timers = Timers(redis_client=redis_client)
timers.include_router(router)
```

4. Run the timer processing loop:
```python
await timers.run_forever()
```

## Development Conventions

### Code Style

- Follow PEP 8 coding standards
- Use type hints for all function parameters and return values
- Use Ruff for linting and formatting
- Maintain 120 character line length limit
- Use dataclasses with `kw_only=True, slots=True, frozen=True` for immutable objects

### Testing

- Use pytest for unit testing
- Place tests in the `tests/` directory
- Name test files with `test_` prefix
- Use descriptive test function names
- Test both positive and negative cases

### Documentation

- Use docstrings for all public functions and classes
- Follow Google-style docstring format
- Document parameter types and return values
- Include usage examples for complex functionality

### Git Workflow

- Create feature branches for new functionality
- Write clear, concise commit messages
- Keep commits focused on single changes
- Rebase on main branch before merging
- Ensure all tests pass before pushing

## Key Features

### Timer Scheduling

Timers can be scheduled with:
- A topic identifier
- A unique timer ID
- A Pydantic model payload
- An activation period (timedelta)

```python
await timers.set_timer(
topic="my_topic",
timer_id="unique_id",
payload=MyPayload(message="Hello"),
activation_period=timedelta(minutes=5)
)
```

### Handler Registration

Handlers are registered using decorators:

```python
@router.handler(name="custom_name", schema=MySchema)
async def my_handler(data: MySchema):
# Process timer event
pass
```

If no name is provided, the function name is used as the topic.

### Distributed Locking

The library implements two types of locks:
1. **Timer Lock** - Prevents concurrent modifications to the same timer
2. **Consume Lock** - Ensures timers are processed only once in distributed environments

### Automatic Cleanup

Processed timers are automatically removed from Redis to prevent accumulation of stale data.

## Configuration

Environment variables can be used to configure Redis key names:
- `TIMERS_TIMELINE_KEY` - Key for the Redis sorted set storing timer timestamps (default: "timers_timeline")
- `TIMERS_PAYLOADS_KEY` - Key for the Redis hash storing timer payloads (default: "timers_payloads")
- `TIMERS_SEPARATOR` - Separator used between topic and timer ID (default: "--")

## Testing Approach

Tests focus on verifying:
- Handler registration and routing
- Timer scheduling and removal
- Payload validation with Pydantic
- Error handling for missing handlers
- Correct timer key construction

The test suite uses standard pytest patterns with async support.
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM artifactory.raiffeisen.ru/python-community-docker/python:3.13.5-slim-rbru

ARG ARTIFACTORY_USER
ARG ARTIFACTORY_PASSWORD

ENV UV_DEFAULT_INDEX=https://$ARTIFACTORY_USER:[email protected]/artifactory/api/pypi/remote-pypi/simple \
UV_INDEX_RAIF_USERNAME=$ARTIFACTORY_USER \
UV_INDEX_RAIF_PASSWORD=$ARTIFACTORY_PASSWORD \
UV_INDEX_COMMUNITY_USERNAME=$ARTIFACTORY_USER \
UV_INDEX_COMMUNITY_PASSWORD=$ARTIFACTORY_PASSWORD \
UV_INDEX_TEAM_USERNAME=$ARTIFACTORY_USER \
UV_INDEX_TEAM_PASSWORD=$ARTIFACTORY_PASSWORD \
UV_PROJECT_ENVIRONMENT=/usr/local \
UV_LINK_MODE=copy \
UV_NO_MANAGED_PYTHON=1

COPY pyproject.toml uv.lock ./

RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --all-extras --no-install-project

COPY . .
35 changes: 35 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
default: install lint build test

down:
docker compose down --remove-orphans

sh:
docker compose run --service-ports application bash

test *args: down && down
docker compose run application uv run --no-sync pytest {{ args }}

build:
docker compose build application

install:
uv lock --upgrade
uv sync --all-extras --all-groups --frozen

lint:
uv run end-of-file-fixer .
uv run ruff format
uv run ruff check --fix
uv run mypy .

lint-ci:
uv run end-of-file-fixer . --check
uv run ruff format --check
uv run ruff check --no-fix
uv run mypy .

publish:
rm -rf dist
uv version $GITHUB_REF_NAME
uv build
uv publish --token $PYPI_TOKEN
26 changes: 26 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
services:
application:
build:
context: .
dockerfile: ./Dockerfile
restart: always
volumes:
- .:/srv/www/
depends_on:
redis:
condition: service_healthy
environment:
- REDIS_URL=redis://redis:6379/0
stdin_open: true
tty: true

redis:
image: redis:8
restart: always
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 1s
timeout: 3s
retries: 30
Loading
Loading