aiondemand / aiondemand/aiondemand

[ENH] Add progress indicators for long-running operations

Ouverte
#107 1 commentaire 0 réactions 0 personnes assignées Voir sur GitHub
Langage dominant
Python
Étoiles
38
Forks
89
Métriques de merge des PR
Aucune PR mergée en 30 j

Description

I've recently applied to ESoC 2026 and have been exploring the AI on Demand repository to understand the codebase and identify areas where I can contribute. During my exploration, I came across this user experience issue that affects long-running async operations. I believe addressing this would significantly improve the developer experience when working with the SDK, especially for users fetching large amounts of data.

## Description

Long-running operations such as `get_assets_async()` and `get_list_async()` provide no progress feedback to users. When fetching large amounts of data or a large number of assets, users have no visibility into whether the operation is progressing, stuck, or frozen. This leads to poor user experience, especially for operations that may take several seconds or minutes to complete.

## Problem

Currently, these operations provide no feedback during execution:

```python
import aiod

# Fetching 1000 datasets - how long will this take? Is it working?
datasets = await aiod.datasets.get_list_async(limit=1000, batch_size=50)

# Fetching metadata for 500 identifiers - no indication of progress
identifiers = [f"data_{i}" for i in range(500)]
data = await aiod.datasets.get_assets_async(identifiers)
```

**User experience issues:**
- ❌ No indication that the operation is progressing
- ❌ Users may think the program is frozen/hung
- ❌ No way to estimate time remaining
- ❌ Users might prematurely kill the process
- ❌ Difficult to debug slow operations

## Proposed Solution

Add optional progress indicators using Python's built-in `logging` module for minimal dependencies and maximum flexibility. This approach:

- ✅ **No new dependencies** - uses only Python stdlib
- ✅ **Opt-in via logging configuration** - users control verbosity
- ✅ **Backward compatible** - existing code unchanged
- ✅ **Works everywhere** - terminals, notebooks, scripts, production
- ✅ **Follows best practices** - uses standard logging patterns

### Implementation Details

#### 1. Add logging to async functions

**File: `src/aiod/calls/calls.py`**

Add progress logging at key points in async operations:

```python
import asyncio
import logging
from functools import partial
from http import HTTPStatus
from typing import Literal

import aiohttp
import pandas as pd
import requests

# Add at module level
logger = logging.getLogger(__name__)

# ... existing code ...

async def get_assets_async(
identifiers: list[str],
*,
asset_type: str,
version: str | None = None,
data_format: Literal["pandas", "json"] = "pandas",
) -> pd.DataFrame | list[dict]:
"""Asynchronously retrieve metadata for a list of ASSET_TYPE identifiers.

All parameters except `identifiers` must be specified by name.

Parameters
----------
identifiers
The list of identifiers of the ASSET_TYPE to retrieve.
version
The version of the endpoint (default is None).
data_format
The desired format for the response (default is "pandas").
For "json" formats, the returned type is a json decoded type, in this case a list of dicts.

Returns
-------
:
The retrieved metadata for the specified ASSET_TYPE.

Notes
-----
To see progress information, configure logging at INFO level:

>>> import logging
>>> logging.basicConfig(level=logging.INFO)
>>> # Now async operations will show progress
"""
total = len(identifiers)
logger.info(f"Fetching {total} {asset_type} assets...")

urls = [url_to_get_asset(asset_type, identifier, version) for identifier in identifiers]
response_data = await _fetch_resources(urls, description=f"{asset_type} assets")

logger.info(f"Successfully fetched {total} {asset_type} assets")
resources = format_response(response_data, data_format)
return resources

async def get_list_async(
*,
asset_type: str,
offset: int = 0,
limit: int = 100,
batch_size: int = 10,
version: str | None = None,
data_format: Literal["pandas", "json"] = "pandas",
) -> pd.DataFrame | list[dict]:
"""Asynchronously retrieve a list of ASSET_TYPE from the catalogue in batches.

All parameters must be specified by name.

Parameters
----------
offset: The offset for pagination (default is 0).
limit: The maximum number of items to retrieve (default is 10).
batch_size: The number of items in a batch.
version: The version of the endpoint (default is None).
data_format: The desired format for the response (default is "pandas").
For "json" formats, the returned type is a json decoded type, in this case a list of dicts.

Returns
-------
:
The retrieved metadata in the specified format.

Raises
------
ValueError
Batch size must be larger than 0.

Notes
-----
To see progress information, configure logging at INFO level:

>>> import logging
>>> logging.basicConfig(level=logging.INFO)
>>> # Now async operations will show progress
"""
if batch_size <= 0:
raise ValueError("batch_size must be larger than 0, otherwise you can use the synchronous get_list function!")

offsets = range(offset, offset + limit, batch_size)
last_batch_size = (limit % batch_size) or batch_size
batch_sizes = [batch_size] * (len(offsets) - 1) + [last_batch_size]

num_batches = len(offsets)
logger.info(f"Fetching {limit} {asset_type} in {num_batches} batches (batch_size={batch_size})...")

urls = [url_to_get_list(asset_type, offset, limit, version) for offset, limit in zip(offsets, batch_sizes, strict=False)]

response_data = await _fetch_resources(urls, description=f"{asset_type} batches")

flattened_response_data = [response for batch in response_data for response in batch]
logger.info(f"Successfully fetched {len(flattened_response_data)} {asset_type} items")

resources = format_response(flattened_response_data, data_format)
return resources

async def _fetch_resources(urls: list[str], description: str = "resources") -> list[dict]:
"""Fetch multiple resources asynchronously with progress logging.

Parameters
----------
urls
List of URLs to fetch
description
Description for progress messages (e.g., "datasets", "batches")

Returns
-------
:
List of JSON responses
"""
total = len(urls)

async def _fetch_data(session, url, idx) -> dict:
try:
async with session.get(url, timeout=config.request_timeout_seconds) as response:
result = await response.json()
# Log progress every 10% or every 10 items, whichever is less frequent
progress_interval = max(1, min(10, total // 10))
if (idx + 1) % progress_interval == 0 or (idx + 1) == total:
logger.info(f"Progress: {idx + 1}/{total} {description} fetched ({100 * (idx + 1) // total}%)")
return result
except Exception as e:
logger.error(f"Failed to fetch {description} from {url}: {e}")
raise

async with aiohttp.ClientSession() as session:
tasks = [_fetch_data(session, url, idx) for idx, url in enumerate(urls)]
response_data = await asyncio.gather(*tasks)

return response_data
```

#### 2. Usage Examples

**Default behavior (no change for existing users):**
```python
import aiod

# No progress output (existing behavior)
datasets = await aiod.datasets.get_list_async(limit=1000, batch_size=50)
```

**With progress logging enabled:**
```python
import logging
import aiod

# Configure logging to see progress
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# Now you see progress!
datasets = await aiod.datasets.get_list_async(limit=1000, batch_size=50)
```

**Output:**
```
2026-02-21 10:15:30 - aiod.calls.calls - INFO - Fetching 1000 datasets in 20 batches (batch_size=50)...
2026-02-21 10:15:32 - aiod.calls.calls - INFO - Progress: 2/20 datasets batches fetched (10%)
2026-02-21 10:15:34 - aiod.calls.calls - INFO - Progress: 4/20 datasets batches fetched (20%)
2026-02-21 10:15:36 - aiod.calls.calls - INFO - Progress: 6/20 datasets batches fetched (30%)
...
2026-02-21 10:15:48 - aiod.calls.calls - INFO - Progress: 20/20 datasets batches fetched (100%)
2026-02-21 10:15:48 - aiod.calls.calls - INFO - Successfully fetched 1000 datasets items
```

**For Jupyter notebooks (prettier output):**
```python
import logging
import aiod

# Configure with simpler format for notebooks
logging.basicConfig(level=logging.INFO, format='%(message)s')

datasets = await aiod.datasets.get_list_async(limit=500, batch_size=25)
```

**Output:**
```
Fetching 500 datasets in 20 batches (batch_size=25)...
Progress: 2/20 datasets batches fetched (10%)
Progress: 4/20 datasets batches fetched (20%)
...
Successfully fetched 500 datasets items
```

#### 3. Documentation Updates

**Add to `docs/api/aiod.md` or appropriate API docs:**

```markdown
## Progress Indicators for Async Operations

Async functions like `get_assets_async()` and `get_list_async()` can provide progress
feedback through Python's logging system.

To enable progress indicators, configure logging at INFO level:

\`\`\`python
import logging
import aiod

# Enable progress output
logging.basicConfig(level=logging.INFO, format='%(message)s')

# Now async operations show progress
datasets = await aiod.datasets.get_list_async(limit=1000, batch_size=50)
\`\`\`

For more control, configure the `aiod.calls.calls` logger specifically:

\`\`\`python
import logging

logger = logging.getLogger('aiod.calls.calls')
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(message)s'))
logger.addHandler(handler)
\`\`\`
```

#### 4. Add Example to Notebooks

Update `docs/examples/getting-started.ipynb` with a cell demonstrating progress:

```python
# Cell: Enable progress indicators
import logging
logging.basicConfig(level=logging.INFO, format='%(message)s')

# Cell: Fetch with progress shown
datasets = await aiod.datasets.get_list_async(limit=100, batch_size=10)
```

## Implementation Plan

1. **Modify `src/aiod/calls/calls.py`:**
- Add `logger` at module level
- Update `get_assets_async()` with progress logging
- Update `get_list_async()` with progress logging
- Update `_fetch_resources()` to accept description and log progress
- Update docstrings with logging notes

2. **Update Documentation:**
- Add section on progress indicators to API docs
- Update async function docstrings
- Add example to getting-started notebook

3. **Testing:**
- Verify logging output appears when enabled
- Verify no output when logging disabled (default)
- Test in different environments (terminal, notebook)
- Verify no performance impact

## Benefits of This Approach

1. **Zero dependencies** - Uses Python's standard library
2. **Flexible** - Users control verbosity, format, and destination
3. **Production-ready** - Logs can be captured by logging infrastructure
4. **Non-breaking** - Existing code works exactly as before
5. **Debuggable** - Logs provide diagnostic information, not just progress
6. **Standard practice** - Libraries commonly use logging for progress/status

## Alternative Approaches Considered

### Alternative 1: `tqdm` library
**Pros:** Beautiful progress bars, well-known
**Cons:** New dependency, doesn't work well in all environments, harder to integrate with logging systems
**Decision:** Rejected due to added dependency

### Alternative 2: `show_progress` parameter
**Pros:** Explicit control
**Cons:** API surface increase, doesn't integrate with logging systems, less flexible
**Decision:** Rejected in favor of logging-based approach

### Alternative 3: Callback function
**Pros:** Maximum flexibility
**Cons:** Complex API, harder to use
**Decision:** Rejected as too complex for the benefit

## Acceptance Criteria

- [ ] Add logging statements to `get_assets_async()` showing start/completion
- [ ] Add logging statements to `get_list_async()` showing batches and progress
- [ ] Update `_fetch_resources()` to log periodic progress
- [ ] Progress logs every ~10% or every 10 items (whichever is less frequent)
- [ ] No performance impact when logging is disabled (default)
- [ ] Update docstrings with logging usage notes
- [ ] Add progress indicator section to documentation
- [ ] Add example to notebook showing how to enable progress
- [ ] Test in terminal and Jupyter environments
- [ ] Backward compatible - existing code unchanged

## Open Questions

1. **Logging level:** Should progress be INFO or DEBUG level?
- INFO = Users see progress when they enable info logging (recommended)
- DEBUG = Users need debug logging (more verbose, includes other debug info)

2. **Progress frequency:** Log every 10% okay, or prefer different interval?

3. **Logger name:** Is `aiod.calls.calls` the right logger, or prefer `aiod.progress`?

4. **Error logging:** Should failed requests be logged at ERROR level in `_fetch_resources()`?

---

**Labels**: `enhancement`, `user-experience`, `european-summer-of-code-2026`
**Effort**: Small-Medium
**Priority**: Good to have (quality of life improvement)
**Breaking Changes**: None

Guide de contribution

Ouvrir le guide de contribution

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.