# Changelog

## [0.7.6] - 2026-03-15

### Added
- Added `DefaultAzureCredential` support to `AzureTableJobStore` via `AZURE_STORAGE_ACCOUNT` with optional `AZURE_STORAGE_TABLE_ENDPOINT`.
- Added `AzureTableJobStore.close()` and sync context-manager support so table clients can be released explicitly.

### Changed
- Azure Table client caching now uses a lease-aware shared cache instead of a bare `functools.lru_cache`, allowing shared clients to be reused safely while still cleaning up evicted entries.
- `build_manifest()` and `RunStorageMixin.build_result_manifest()` now return concrete manifest dictionaries, matching how callers consume the payload.
- Documented that Azure Function HTTP clients do not use Azure Identity, and expanded the Azure Table setup examples to show both connection-string and token-based auth modes.
- Expanded scenario-driven regression coverage across HTTP, polling, runtime, and Azure storage helpers, and refactored several clients around narrower test seams instead of private-state mutation.

### Fixed
- Preserved the legacy positional `TableConfig(connection_string, table_name)` constructor while making connection strings optional for token-based auth.
- Closing or resetting Azure Table clients now closes the underlying table client, service client, and credential when no leases remain, avoiding leaked token-auth transports.
- Clarified that zero-lease Azure Table cache entries are evicted intentionally so credentials and transports close deterministically after the last live store releases them.
- `poll_until()` and `poll_until_async()` now respect timeout deadlines before the first fetch after `initial_delay`, and Azure blob tree downloads skip directory-marker blobs instead of trying to download them as files.
- `TaskRuntime` now cancels in-flight work promptly from external cancel events, waits for cleanup during `aclose()`, and avoids cancellation races that could drop successful completions or consume the next lazy task.
- URL helpers now append path segments without mangling query-bearing base URLs, and sync/async HTTP retry helpers no longer retry malformed URL errors that can never succeed.
- Shared async HTTP and asset clients now close stale or closed-loop transports before replacement, and `AzureFunctionSyncClient.close()` now shuts down its owned async client even after the background loop has already stopped.
- Heartbeat and Azure storage fallback writes now preserve legacy no-`raise_on_error` call signatures without double-invoking client methods when the underlying implementation raises `TypeError`.

## [0.7.4] - 2026-03-12

### Added
- Added `LoopBoundAsyncLock` to safely provision per-loop `asyncio.Lock` instances in shared clients.
- Added `AzureFunctionAsyncClient.close()` so owned HTTP resources can be released explicitly.

### Changed
- `AzureFunctionSyncClient.close()` now shuts down the owned async HTTP client before stopping its background event loop.
- HTTP helper logs now sanitize URLs before emitting retry and rate-limit metadata.
- `poll_until_async()` now accepts synchronous wrappers that return awaitables, such as `lambda` wrappers and `functools.partial(...)`.

### Fixed
- Escaped single quotes in Azure Table OData filters and made `TableConfig.from_env(env={})` honor an explicitly empty environment mapping.
- Switched synchronous polling deadlines to `time.monotonic()` so wall-clock changes do not skew timeout handling.
- Offloaded blocking cleanup in `AzureStorageClient.aclose()` to avoid stalling the active event loop.
- Made shared async lock creation and sync loop runner initialization thread-safe across event loops and threads.
- Synchronized heartbeat thread exception propagation and local persistence to avoid stale or torn writes.
- Skipped Azure blob listing in `build_manifest()` when the Azure client is not configured.
- Preserved pre-start cancellation in `TaskRuntime.cancel()` so a cancel request is not lost before runtime primitives are created.

## [0.7.3] - 2026-01-15

### Changed
- Declare Python 3.10 support in package metadata, tooling targets, and docs.
- TaskRuntime now uses `asyncio.wait_for` for per-task timeouts to keep Python 3.10 compatible.

### Fixed
- Consume cancelled task exceptions during shutdown to avoid "Task exception was never retrieved" warnings.
- Use `timezone.utc` for monitoring timestamps to keep Python 3.10 compatible.

## [0.7.2] - 2025-12-02

### Changed
- AzureStorageClient now raises on upload failures by default (`swallow_errors=False`); set `swallow_errors=True` explicitly if you need best-effort mirroring.
- Azure Table list_runs fails fast when `query_entities` is unavailable to avoid unfiltered scans; ensures environments use `azure-data-tables`/clients that support filtered paging.

### Fixed
- Tests updated for the stricter upload behavior and query_entities requirement; README troubleshooting reflects the new default.

## [0.7.1] - 2025-12-02

### Fixed
- Azure Table pagination now uses `query_entities` so filters persist across pages and continuation tokens are returned, preventing unfiltered table scans and enabling paging.
- Blob URLs now respect `BlobEndpoint`/SAS values from connection strings and append SAS tokens, producing valid links for private/custom endpoints.
- Container creation errors are no longer fatal for read/list-only credentials, allowing download/list operations to proceed in best-effort/read-only modes.
- Heartbeat uploads now force `raise_on_error` to surface critical failures even when clients are configured to swallow upload errors.

### Added
- Declared `azure-data-tables` in the `azure` extra to make Table helpers installable without manual dependency management.

## [0.7.0] - 2025-12-01

### Added
- `AzureStorageSettings.from_env_with_prefix(prefix)` factory to load service-specific env vars (e.g., `CRAWLER_AZURE_STORAGE_CONTAINER`).
- `from_env()` now returns `(settings, mode)` tuple and accepts `mode_var` parameter.
- `StorageMode` type alias (`"strict"`, `"best-effort"`, `"dev-skip"`).
- `reset_all_clients()` helper to clear all cached clients (useful for testing).
- `is_configured(settings, mode)` function to check if settings would produce a configured client.
- `TelemetryConfig` dataclass with `from_env()` and `from_env_with_prefix()` factories for explicit telemetry configuration.
- `reset_table_client_cache()` to clear cached Azure Table clients (for testing and config changes).
- Atexit handler for `AzureFunctionSyncClient` to ensure daemon threads clean up on process exit.
- Event loop tracking for `AssetDownloadClient` shared client to prevent cross-loop access errors.
- Async lock for storage client cache operations to avoid blocking event loops under high concurrency.

### Changed
- **BREAKING**: `AzureStorageSettings.from_env()` now returns `(settings, mode)` tuple instead of just settings.
- **BREAKING**: `get_client()` and `get_shared_client()` now require `settings` and `mode` parameters (no implicit env fallback).
- **BREAKING**: `AzureStorageClient.__init__()` and `from_settings()` now require `settings` and `mode` parameters.
- **BREAKING**: `RunLogWriter` and `HeartbeatMonitor` now require `azure_client` parameter (no implicit fallback).
- **BREAKING**: `RunStorageMixin` now requires `azure_client` parameter.
- **BREAKING**: `build_manifest()` now requires `client` parameter.
- **BREAKING**: Removed module-level helpers (`upload_text`, `download_tree`, `blob_path_for`, etc.) - use client methods directly.
- **BREAKING**: Removed `enabled` parameter from client (use `mode="dev-skip"` instead).
- `TelemetryClient` now accepts optional `config: TelemetryConfig` parameter for explicit configuration.
- Deprecation warning emitted when `TelemetryClient` is instantiated without explicit config.

### Removed
- Module-level convenience functions (`upload_text`, `upload_file`, `download_tree`, `download_to_path`, `list_tree`, `blob_path_for`, and their async variants).
- Implicit environment variable fallback in `get_client()`.
- `is_enabled()` function (replaced by `is_configured()`).

### Migration

**Before (implicit configuration):**
```python
from infra_core.azure import get_client, upload_text

client = get_client()  # Reads AZURE_STORAGE_* from env
upload_text(path, text)  # Uses implicit client
```

**After (explicit configuration):**
```python
from infra_core.azure import AzureStorageSettings, get_client

# Option 1: Use prefixed env vars for service isolation
settings, mode = AzureStorageSettings.from_env_with_prefix("CRAWLER")
# Reads: CRAWLER_AZURE_STORAGE_CONTAINER, CRAWLER_AZURE_STORAGE_MODE, etc.

# Option 2: Use default env vars (still explicit)
settings, mode = AzureStorageSettings.from_env()
# Reads: AZURE_STORAGE_CONTAINER, AZURE_STORAGE_MODE, etc.

client = get_client(settings, mode)
client.upload_text(path, text)
```

**Monitoring classes:**
```python
# Before
writer = RunLogWriter(storage_root)  # Implicit client

# After
settings, mode = AzureStorageSettings.from_env_with_prefix("MYSERVICE")
client = get_client(settings, mode)
writer = RunLogWriter(storage_root, azure_client=client)
```

## [0.6.0] - 2025-11-22

### Added
- Azure storage mode selection (`strict`, `best-effort`, `dev-skip`) via `mode=` parameters or `AZURE_STORAGE_MODE`, plus `ConfigurationError` for invalid configurations and reason-tagged skip logs.
- Module-level helpers accept `mode=` to override behavior programmatically; `enabled=False` now maps explicitly to `dev-skip` for local-only runs.

### Changed
- **BREAKING**: Azure storage now defaults to `strict` mode. Missing container or credentials raise immediately instead of silently skipping uploads; use `mode=dev-skip`/`AZURE_STORAGE_MODE=dev-skip` to preserve previous no-op behavior in local dev, or `best-effort` to log-and-skip when unconfigured.

### Migration
- Production: ensure `AZURE_STORAGE_CONTAINER` plus either `AZURE_STORAGE_CONNECTION_STRING` or `AZURE_STORAGE_ACCOUNT`/`AZURE_STORAGE_BLOB_ENDPOINT` are set; expect `ConfigurationError` if not.
- Local/dev: opt into `mode=dev-skip` (or `enabled=False`) to keep local-only writes, or `mode=best-effort` to log-and-skip when config is missing.

## [0.5.0] - 2025-11-21

### Added
- Optional fail-fast path for `HeartbeatMonitor` via `interrupt_main_on_error` flag to immediately surface critical heartbeat upload failures to the main thread.
- `on_error` callback hook on `HeartbeatMonitor` to let callers handle background failures explicitly.
- Expanded telemetry surface with upload reliability, resume, and critical upload events for richer instrumentation.

### Changed
- Heartbeat upload failures are now treated as critical: background thread exceptions are stored and re-raised on the next `set_slug`/`pulse`/`stop` call, eliminating silent thread death.
- Heartbeat uploads log success only after an actual upload attempt and raise if no client upload method is available, preventing false positives.

### Fixed
- Added regression tests ensuring heartbeat upload failures propagate (including optional main-thread interrupt) and that uploads succeed through both `write_json` and `upload_file` paths.

## [0.4.0] - 2025-11-20

### Added
- **Enhanced manifest structure** for Azure storage with enriched file metadata including file types, MIME content types, and full blob URLs.
- **Automatic MIME type detection** using Python's `mimetypes` standard library for comprehensive file type support (hundreds of types).
- **Summary statistics** in manifests with total file counts, total size in bytes, and file type breakdowns.
- **Generic file categorization** system (image, document, data, other) for flexible manifest organization across different workloads.
- **Proper URL encoding** for blob names using `urllib.parse.quote` to correctly handle special characters (spaces, unicode, `#`, `?`, `&`, etc.).
- **Type-safe helper functions** with comprehensive Google-style docstrings for `_infer_file_type`, `_infer_content_type`, `_build_file_entries`, `_build_storage_section`, `_build_summary`, and `_get_file_size`.
- **24 new parametrized tests** for file type inference, content type detection, and edge case coverage.

### Changed
- **BREAKING**: Manifest structure changed from `{"storage_path": str, "files": list, "blob_prefix": str}` to `{"files": list, "storage": dict, "summary": dict}`. The new structure provides storage metadata in a dedicated `storage` section with provider type, container, and prefix fields.
- Moved `import re` to module level in `storage.py` for better code organization.
- Enhanced all helper function docstrings to follow Google style guide with Args, Returns, and Example sections.

### Fixed
- Type safety improvements with proper type guards for `_build_summary()` to eliminate mypy errors.

### Migration Guide
For code using the old manifest format:
```python
# Old format
storage_path = manifest["storage_path"]
blob_prefix = manifest["blob_prefix"]

# New format
storage = manifest.get("storage", {})
storage_path = str(root)  # Not included in manifest
blob_prefix = storage.get("prefix")
```

The new manifest structure follows 2025 REST API best practices with:
- Full blob URLs for direct file access
- Rich metadata for client-side filtering and display
- Summary statistics for efficient UI rendering

## [0.3.2] - 2025-11-19

### Fixed
- Fix critical event loop closure bug in Azure async storage operations (`upload_file_async`, `upload_text_async`) that caused "RuntimeError: Event loop is closed" when processing multiple concurrent uploads in Azure Functions.
- Implement event loop detection in `container_async()` to automatically close and recreate async clients (`AsyncBlobServiceClient`, `AsyncContainerClient`) when the event loop changes, preventing stale client references across event loop boundaries.
- Add comprehensive tests for event loop lifecycle management, concurrent async uploads (multi-site batch scenarios), and error handling to prevent silent upload failures.

## [0.3.1] - 2025-11-19

### Fixed
- Fix event loop lifecycle management in sync client wrappers to prevent "RuntimeError: Event loop is closed" when making multiple sequential API calls.
- Implement persistent background event loop in thread for `AzureFunctionSyncClient` and `download_asset()` sync wrapper, replacing the problematic `asyncio.run()` pattern that created and destroyed loops on each call.
- Restore connection pooling efficiency in `download_asset()` by using the shared client instead of creating a new httpx client per download.
- Simplify event loop cleanup logic by removing unnecessary global loop tracking and overly defensive cross-loop client disposal code.

## [0.3.0] - 2025-11-18
- Add reusable polling utilities in `infra_core.polling` module for status polling with exponential backoff.
- Implement `poll_until()` and `poll_until_async()` functions for generic polling with configurable timeout and intervals.
- Add `StatusPoller` class for high-level status-based polling with terminal and failure state detection.
- Include `PollingConfig` dataclass for centralized polling configuration with validation.
- Create custom exception hierarchy (`PollingError`, `PollingTimeoutError`, `PollingFailureError`) for polling operations.
- Add comprehensive test suite for polling module covering sync/async polling, timeouts, failures, and edge cases.
- Include `@runtime_checkable` Protocol for status extraction to enable both dict and object responses.
- Add ruff configuration for code quality and consistency checks.
- Harden `PollingConfig` with `frozen=True`, `kw_only=True`, and `slots=True`, convert mutable status sets to `frozenset`, and wire up configurable `initial_delay`, `backoff_multiplier`, and `max_backoff` across sync/async pollers.
- Expand `.pre-commit-config.yaml` to include the full readiness checklist (formatting, security, validation hooks) and document hook installation in `CONTRIBUTING.md`.

## [0.2.0] - 2025-11-13
- Improve Azure helper typing coverage by annotating the HTTP response builders and Table Storage job store internals (mypy now passes without ignores).
- Add `tests/test_azure_storage_helpers.py` to push coverage above 95% and exercise the blob streaming/atomic replacement helpers.
- Expand the README with supported Python versions, Azure env snippets, and CLI wiring examples; enable Python 3.13 in CI so tests run on 3.11–3.13.
- Keep the release readiness notes and changelog scaffold alongside the package so upcoming PyPI drops stay auditable.

## [0.1.0] - 2025-11-07
- Initial release covering the core HTTP clients, async runtime, logging utilities, storage helpers, and CLI integrations shared across services.
- Ship the async HTTP helpers, storage abstractions, and supporting tests that together enable telemetry-driven crawlers and Azure deployments.
- Document the tooling snapshot (uv lock, pytest, ruff, mypy) and package metadata that underpin the PyPI-ready distribution.
