mdfetch 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.github/workflows/integration.yml +0 -3
- mdfetch-0.2.2/.specify/feature.json +3 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/memory/changelog.md +17 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/memory/plan.md +7 -5
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/memory/spec.md +20 -4
- {mdfetch-0.2.1 → mdfetch-0.2.2}/CLAUDE.md +3 -1
- {mdfetch-0.2.1 → mdfetch-0.2.2}/PKG-INFO +1 -1
- {mdfetch-0.2.1 → mdfetch-0.2.2}/pyproject.toml +1 -1
- mdfetch-0.2.2/specs/004-remove-backoff/checklists/requirements.md +34 -0
- mdfetch-0.2.2/specs/004-remove-backoff/plan.md +146 -0
- mdfetch-0.2.2/specs/004-remove-backoff/research.md +42 -0
- mdfetch-0.2.2/specs/004-remove-backoff/spec.md +92 -0
- mdfetch-0.2.2/specs/004-remove-backoff/tasks.md +147 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/__init__.py +2 -2
- {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/base.py +6 -7
- {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/providers/medium.py +31 -1
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/architecting-the-asynchronous-agent.md +97 -146
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/from-drift-to-parity.md +19 -19
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/integration-digest-december-2025.md +5 -5
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/test_devto_integration.py +2 -7
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/test_medium_integration.py +2 -7
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/test_fetch_errors.py +3 -24
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/test_medium_extractor.py +29 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/uv.lock +1 -1
- mdfetch-0.2.1/.specify/feature.json +0 -3
- mdfetch-0.2.1/tests/integration/conftest.py +0 -17
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-analyze/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-archive-run/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-checklist/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-clarify/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-constitution/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-implement/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-plan/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-reconcile-run/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-specify/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-tasks/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.analyze.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.archive.run.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.checklist.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.clarify.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.constitution.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.implement.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.plan.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.reconcile.run.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.specify.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.tasks.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gemini/commands/speckit.taskstoissues.toml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.github/workflows/ci.yml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.github/workflows/publish.yml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.gitignore +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.python-version +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/.registry +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/archive/LICENSE +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/archive/README.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/archive/commands/archive.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/archive/extension.yml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/README.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/config-template.yml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/extension.yml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/git-config.yml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/reconcile/LICENSE +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/reconcile/README.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/reconcile/commands/reconcile.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions/reconcile/extension.yml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/extensions.yml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/init-options.json +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/integration.json +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/integrations/claude.manifest.json +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/integrations/gemini.manifest.json +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/integrations/speckit.manifest.json +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/memory/constitution.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/scripts/bash/check-prerequisites.sh +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/scripts/bash/common.sh +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/scripts/bash/create-new-feature.sh +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/scripts/bash/setup-plan.sh +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/scripts/bash/setup-tasks.sh +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/templates/checklist-template.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/templates/constitution-template.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/templates/plan-template.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/templates/spec-template.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/templates/tasks-template.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/workflows/speckit/workflow.yml +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/.specify/workflows/workflow-registry.json +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/GEMINI.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/LICENSE +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/Makefile +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/README.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/checklists/requirements.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/contracts/api.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/data-model.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/plan.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/quickstart.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/research.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/spec.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/001-mdfetch-medium-extractor/tasks.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/checklists/requirements.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/contracts/public-api.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/data-model.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/plan.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/quickstart.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/research.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/spec.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/002-devto-provider/tasks.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/checklists/requirements.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/contracts/extract-api.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/plan.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/research.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/spec.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/specs/003-medium-freedium-fallback/tasks.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/exceptions.py +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/providers/__init__.py +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/providers/devto.py +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/src/mdfetch/router.py +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/__init__.py +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/conftest.py +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/__init__.py +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/devto-integration-digest-december-2025.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/devto-integration-digest-july-2025.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/integration/snapshots/devto-integration-digest-march-2026.md +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/__init__.py +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/test_devto_extractor.py +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/test_router.py +0 -0
- {mdfetch-0.2.1 → mdfetch-0.2.2}/tests/unit/test_silent.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
### mdfetch — Remove Exponential Backoff — 2026-05-15
|
|
6
|
+
|
|
7
|
+
**Branch**: `004-remove-backoff`
|
|
8
|
+
**Spec**: specs/004-remove-backoff
|
|
9
|
+
|
|
10
|
+
**What was added**:
|
|
11
|
+
- Fixed-delay retry behaviour: `fetch_html` now sleeps exactly `retry_delay` seconds between attempts (previously exponential `retry_delay × 2ⁿ`, capped at 60 s). The change makes retry timing predictable for callers and aligns with the Freedium fallback that already absorbs 403/429 errors.
|
|
12
|
+
- Removed `MDFETCH_RETRIES` and `MDFETCH_RETRY_DELAY` env-var support from integration test fixtures (`conftest.py` deleted) and CI workflow (`integration.yml`). Integration tests now hardcode `retries=3, retry_delay=2.0`.
|
|
13
|
+
- Deleted two unit tests (`test_exponential_backoff_sleep_sequence`, `test_exponential_backoff_capped_at_max_delay`) that verified the removed exponential schedule. Added sleep-value assertion to `test_status_code_not_in_no_retry_set_still_retries` to close the FR-029 gap.
|
|
14
|
+
|
|
15
|
+
**New Components**:
|
|
16
|
+
- No new files. Pure removal: `tests/integration/conftest.py` deleted; `_MAX_RETRY_DELAY` constant removed from `src/mdfetch/base.py`.
|
|
17
|
+
|
|
18
|
+
**Tasks Completed**: 16/16
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
5
22
|
### mdfetch — Medium Freedium Fallback — 2026-05-15
|
|
6
23
|
|
|
7
24
|
**Branch**: `003-medium-freedium-fallback`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# mdfetch — Main Implementation Plan
|
|
2
2
|
|
|
3
|
-
**Last Updated**: 2026-05-
|
|
4
|
-
**Sources**: [specs/001-mdfetch-medium-extractor/plan.md], [specs/002-devto-provider/plan.md]
|
|
3
|
+
**Last Updated**: 2026-05-15
|
|
4
|
+
**Sources**: [specs/001-mdfetch-medium-extractor/plan.md], [specs/002-devto-provider/plan.md], [specs/003-medium-freedium-fallback/plan.md], [specs/004-remove-backoff/plan.md]
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -41,6 +41,7 @@ BaseExtractor (ABC) — src/mdfetch/base.py
|
|
|
41
41
|
├── _no_retry_status_codes: frozenset[int] = frozenset() — codes that skip retry; overridden by providers
|
|
42
42
|
├── fetch_html(url, *, retries, retry_delay, _no_retry_codes=None) → str
|
|
43
43
|
│ — streaming HTTP, 30s timeout, 10 MB cap;
|
|
44
|
+
│ fixed delay of retry_delay seconds between attempts (not exponential);
|
|
44
45
|
│ codes in _no_retry_codes (or class attribute) raise immediately
|
|
45
46
|
├── clean_html(soup) → Tag — abstract: platform-specific HTML isolation
|
|
46
47
|
├── convert_to_markdown(tag) → str— abstract: platform-specific Markdown conversion
|
|
@@ -139,7 +140,7 @@ Makefile # setup / test / integration / lint / typecheck / f
|
|
|
139
140
|
|
|
140
141
|
## Testing Strategy
|
|
141
142
|
|
|
142
|
-
**Unit tests** (
|
|
143
|
+
**Unit tests** (63 tests, offline):
|
|
143
144
|
- Router: domain routing, subdomain suffix matching, duplicate registration, invalid URLs, unsupported platforms
|
|
144
145
|
- MediumExtractor: clean_html, convert_to_markdown, empty content, non-article pages, _parse_freedium (heading remap, missing main-content), fallback on 403/429 (URL construction, exc.url contract, no-sleep on 429), no-fallback on 200, UnsupportedContentTypeError.url on Freedium path [003-medium-freedium-fallback]
|
|
145
146
|
- DevToExtractor: clean_html (title/cover/heading/image preservation, iframe/ltag embed→link, anchor stripping, non-article error), convert_to_markdown (headings/code/lists/images, no raw HTML, empty content error) [002-devto-provider]
|
|
@@ -150,7 +151,7 @@ Makefile # setup / test / integration / lint / typecheck / f
|
|
|
150
151
|
- Parametrized over 3 real stn1slv.medium.com articles (including a known paywalled URL that exercises the Freedium fallback when medium.com returns 403) [003-medium-freedium-fallback]
|
|
151
152
|
- Parametrized over 3 real dev.to/stn1slv articles [002-devto-provider]
|
|
152
153
|
- Snapshot-based containment check: `expected_body in extracted_result` — tests pass regardless of whether medium.com or Freedium served the content (heading normalisation ensures identical output)
|
|
153
|
-
- 3 retries with 2-second delay on `FetchError` (
|
|
154
|
+
- 3 retries with 2-second **fixed** delay on `FetchError` (hardcoded; not env-var configurable) [004-remove-backoff]
|
|
154
155
|
- Run with: `make integration` or `uv run pytest tests/integration/ --override-ini=addopts=`
|
|
155
156
|
- Excluded from default `pytest` run via `addopts = "-m 'not integration'"` in pyproject.toml
|
|
156
157
|
|
|
@@ -189,6 +190,7 @@ Makefile # setup / test / integration / lint / typecheck / f
|
|
|
189
190
|
| Medium 403/429 fallback | Override `extract()` in `MediumExtractor`; `_no_retry_status_codes=frozenset({403,429})` on class | Immediate fallback with no medium.com retries; `BaseExtractor` extended with `_no_retry_codes` param for thread safety | [003-medium-freedium-fallback]
|
|
190
191
|
| Freedium HTML parsing | Dedicated `_parse_freedium()` method; `div.main-content`; h4→h3 remap | Freedium HTML is structurally incompatible with `clean_html()` (no `<article>`); heading remap ensures snapshot tests pass for both paths | [003-medium-freedium-fallback]
|
|
191
192
|
| Freedium exc.url contract | `inner_exc.url = url` unconditionally; error message is source-agnostic ("Fallback page…") | Preserves transparent-fallback contract (FR-028); `exc.url` is the authoritative field; message content is internal | [003-medium-freedium-fallback]
|
|
193
|
+
| Retry strategy | Fixed delay (`retry_delay` seconds per attempt, unchanged between attempts) | Exponential backoff removed (PR #8); Freedium fallback absorbs 403/429 at a higher level making exponential growth unnecessary; integration tests use hardcoded defaults (`retries=3, retry_delay=2.0`) with no env-var override | [004-remove-backoff]
|
|
192
194
|
|
|
193
195
|
---
|
|
194
196
|
|
|
@@ -202,4 +204,4 @@ Makefile # setup / test / integration / lint / typecheck / f
|
|
|
202
204
|
|
|
203
205
|
---
|
|
204
206
|
|
|
205
|
-
*Last Updated: 2026-05-15 | Sources appended: [specs/
|
|
207
|
+
*Last Updated: 2026-05-15 | Sources appended: [specs/004-remove-backoff/plan.md]*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# mdfetch — Main Specification
|
|
2
2
|
|
|
3
|
-
**Last Updated**: 2026-05-
|
|
4
|
-
**Sources**: [specs/001-mdfetch-medium-extractor/spec.md], [specs/002-devto-provider/spec.md]
|
|
3
|
+
**Last Updated**: 2026-05-15
|
|
4
|
+
**Sources**: [specs/001-mdfetch-medium-extractor/spec.md], [specs/002-devto-provider/spec.md], [specs/003-medium-freedium-fallback/spec.md], [specs/004-remove-backoff/spec.md]
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -105,6 +105,18 @@ A developer calls the library's extract function for a publicly accessible Mediu
|
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
108
|
+
### US-010 — Predictable Fixed-Delay Retry Behaviour (P1)
|
|
109
|
+
[Source: specs/004-remove-backoff]
|
|
110
|
+
|
|
111
|
+
A developer calling `extract()` encounters a transient network error. The library retries using a simple, predictable fixed delay — exactly `retry_delay` seconds between every attempt — rather than an exponentially growing delay. The retry timing is constant and easy to reason about regardless of which attempt number is being made.
|
|
112
|
+
|
|
113
|
+
**Acceptance Scenarios**:
|
|
114
|
+
1. Given `fetch_html` is called with `retries=3` and `retry_delay=2.0`, when the first two attempts raise a transient error, then the library sleeps exactly `2.0` seconds before each retry (not `2.0` then `4.0`).
|
|
115
|
+
2. Given `fetch_html` is called with `retries=1`, when the attempt fails, then no sleep occurs and the exception is raised immediately.
|
|
116
|
+
3. Given a status code in `_no_retry_status_codes`, when that error is raised, then no sleep or retry occurs.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
108
120
|
### US-006 — Integration Tests Pass Against Real dev.to Article URLs (P3)
|
|
109
121
|
[Source: specs/002-devto-provider]
|
|
110
122
|
|
|
@@ -135,6 +147,7 @@ A developer runs the integration test suite and all dev.to integration tests pas
|
|
|
135
147
|
### Network
|
|
136
148
|
- **FR-006**: The library MUST raise a descriptive error when a network request fails (connection error, timeout, non-2xx HTTP status).
|
|
137
149
|
- **FR-014**: HTTP requests MUST use a standard browser-like User-Agent string so that web servers return readable HTML. The User-Agent MUST NOT identify the library by name or version. The library does not check or respect `robots.txt` in v1.
|
|
150
|
+
- **FR-029**: The `fetch_html` method MUST use a fixed delay of exactly `retry_delay` seconds between retry attempts — no exponential multiplication. The retry delay is constant and does not grow with each attempt number. [Source: specs/004-remove-backoff]
|
|
138
151
|
|
|
139
152
|
### Packaging & Testing
|
|
140
153
|
- **FR-009**: The library MUST be packaged and distributed via PyPI using modern Python packaging best practices, enabling installation through the standard package manager without additional steps.
|
|
@@ -238,7 +251,7 @@ caller provides URL string
|
|
|
238
251
|
- **Network timeouts**: Covered by `FetchError` (30-second fixed timeout).
|
|
239
252
|
- **Oversized responses**: Responses exceeding 10 MB are rejected with `FetchError` to prevent OOM.
|
|
240
253
|
- **Profile/tag pages**: When a `medium.com` URL points to a non-article page, `UnsupportedContentTypeError` is raised (distinct from `UnsupportedPlatformError`).
|
|
241
|
-
- **HTTP 403 / transient failures**: Integration tests use
|
|
254
|
+
- **HTTP 403 / transient failures**: Integration tests use 3 retries with a 2-second fixed delay (hardcoded; not env-var configurable) to handle transient rate limits. [Source: specs/004-remove-backoff]
|
|
242
255
|
- **dev.to profile pages**: When a `dev.to` URL points to an author profile (no `div#article-body`), `UnsupportedContentTypeError` is raised.
|
|
243
256
|
- **dev.to tag listing pages**: When a `dev.to` URL points to a tag page (e.g., `dev.to/t/kafka`), `UnsupportedContentTypeError` is raised.
|
|
244
257
|
- **dev.to liquid-tag embeds**: Embedded third-party widgets (GitHub Gists, CodePen, YouTube) serialised as `<div class="ltag__*" data-url="...">` are replaced with plain Markdown links; they are never silently dropped.
|
|
@@ -273,6 +286,9 @@ caller provides URL string
|
|
|
273
286
|
- The library supports Python 3.12 and later.
|
|
274
287
|
- The library operates on publicly accessible HTML; it does not execute JavaScript or render dynamic content.
|
|
275
288
|
- Network timeouts use a fixed default of 30 seconds (not user-configurable in v1).
|
|
289
|
+
- **SC-018**: `make test` passes with zero failures (all unit tests green) when run without any `MDFETCH_*` environment variables. [Source: specs/004-remove-backoff]
|
|
290
|
+
- **SC-019**: `make integration` passes with zero failures when run without any `MDFETCH_RETRIES` or `MDFETCH_RETRY_DELAY` environment variables — integration tests use hardcoded defaults (3 retries, 2.0 s delay). [Source: specs/004-remove-backoff]
|
|
291
|
+
- **SC-020**: No reference to `MDFETCH_RETRIES`, `MDFETCH_RETRY_DELAY`, or `_MAX_RETRY_DELAY` appears in `src/`, `tests/`, or `.github/` (specification and documentation files excluded). [Source: specs/004-remove-backoff]
|
|
276
292
|
- **SC-013**: Articles that previously failed with a 403 paywall error are successfully extracted in at least 90% of cases where the Freedium mirror has the content available. [Source: specs/003-medium-freedium-fallback]
|
|
277
293
|
- **SC-014**: Articles that previously failed with a 429 rate-limit error are successfully extracted via fallback without requiring the caller to retry. [Source: specs/003-medium-freedium-fallback]
|
|
278
294
|
- **SC-015**: Zero changes are required in existing caller code to benefit from the Freedium fallback — existing integrations continue to work as-is. [Source: specs/003-medium-freedium-fallback]
|
|
@@ -281,4 +297,4 @@ caller provides URL string
|
|
|
281
297
|
|
|
282
298
|
---
|
|
283
299
|
|
|
284
|
-
*Last Updated: 2026-05-15 | Sources appended: [specs/
|
|
300
|
+
*Last Updated: 2026-05-15 | Sources appended: [specs/004-remove-backoff/spec.md]*
|
|
@@ -51,5 +51,7 @@ uv run mypy src/ # type check
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
<!-- SPECKIT START -->
|
|
54
|
-
|
|
54
|
+
## Recent Changes
|
|
55
|
+
|
|
56
|
+
- **004-remove-backoff**: Removed exponential backoff from `fetch_html`; restored fixed-delay retries (`retry_delay` seconds per attempt, constant between attempts). Deleted `MDFETCH_RETRIES`/`MDFETCH_RETRY_DELAY` env-var support from CI and integration fixtures; hardcoded `retries=3, retry_delay=2.0` in all integration tests. Deleted `tests/integration/conftest.py`.
|
|
55
57
|
<!-- SPECKIT END -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mdfetch
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Extract article content from web platforms and return it as clean Markdown.
|
|
5
5
|
Project-URL: Homepage, https://github.com/stn1slv/md-fetch
|
|
6
6
|
Project-URL: Source, https://github.com/stn1slv/md-fetch
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Specification Quality Checklist: Remove Exponential Backoff and Env-Var Retry Config
|
|
2
|
+
|
|
3
|
+
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
|
4
|
+
**Created**: 2026-05-15
|
|
5
|
+
**Feature**: [spec.md](../spec.md)
|
|
6
|
+
|
|
7
|
+
## Content Quality
|
|
8
|
+
|
|
9
|
+
- [X] No implementation details (languages, frameworks, APIs)
|
|
10
|
+
- [X] Focused on user value and business needs
|
|
11
|
+
- [X] Written for non-technical stakeholders
|
|
12
|
+
- [X] All mandatory sections completed
|
|
13
|
+
|
|
14
|
+
## Requirement Completeness
|
|
15
|
+
|
|
16
|
+
- [X] No [NEEDS CLARIFICATION] markers remain
|
|
17
|
+
- [X] Requirements are testable and unambiguous
|
|
18
|
+
- [X] Success criteria are measurable
|
|
19
|
+
- [X] Success criteria are technology-agnostic (no implementation details)
|
|
20
|
+
- [X] All acceptance scenarios are defined
|
|
21
|
+
- [X] Edge cases are identified
|
|
22
|
+
- [X] Scope is clearly bounded
|
|
23
|
+
- [X] Dependencies and assumptions identified
|
|
24
|
+
|
|
25
|
+
## Feature Readiness
|
|
26
|
+
|
|
27
|
+
- [X] All functional requirements have clear acceptance criteria
|
|
28
|
+
- [X] User scenarios cover primary flows
|
|
29
|
+
- [X] Feature meets measurable outcomes defined in Success Criteria
|
|
30
|
+
- [X] No implementation details leak into specification
|
|
31
|
+
|
|
32
|
+
## Notes
|
|
33
|
+
|
|
34
|
+
- All checklist items pass. Specification is ready for `/speckit-clarify` or `/speckit-plan`.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Implementation Plan: Remove Exponential Backoff and Env-Var Retry Config
|
|
2
|
+
|
|
3
|
+
**Branch**: `004-remove-backoff` | **Date**: 2026-05-15 | **Spec**: [spec.md](spec.md)
|
|
4
|
+
|
|
5
|
+
**Input**: Feature specification from `specs/004-remove-backoff/spec.md`
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Replace the exponential backoff formula introduced in PR #6 (`min(60, retry_delay × 2ⁿ)`) with a simple fixed delay (`retry_delay` seconds, unchanged between attempts). Remove the `_MAX_RETRY_DELAY` module constant, remove `MDFETCH_RETRIES`/`MDFETCH_RETRY_DELAY` env-var reads from the integration test fixtures and CI workflow, and delete the two unit tests that verified the exponential schedule. The `extract()` public API signature and exception contract are entirely unchanged.
|
|
10
|
+
|
|
11
|
+
## Technical Context
|
|
12
|
+
|
|
13
|
+
**Language/Version**: Python 3.12+
|
|
14
|
+
|
|
15
|
+
**Primary Dependencies**:
|
|
16
|
+
|
|
17
|
+
| Package | Role |
|
|
18
|
+
|---------|------|
|
|
19
|
+
| `httpx` | HTTP client |
|
|
20
|
+
| `beautifulsoup4` / `lxml` | HTML parsing |
|
|
21
|
+
| `markdownify` | Markdown conversion |
|
|
22
|
+
| `pytest` | Test runner |
|
|
23
|
+
| `ruff` / `mypy` | Lint, format, type-check |
|
|
24
|
+
|
|
25
|
+
**Storage**: N/A
|
|
26
|
+
|
|
27
|
+
**Testing**: pytest; unit tests for retry logic; integration tests against real URLs
|
|
28
|
+
|
|
29
|
+
**Target Platform**: Cross-platform PyPI library (Linux, macOS, Windows); Python 3.12+
|
|
30
|
+
|
|
31
|
+
**Project Type**: library
|
|
32
|
+
|
|
33
|
+
**Performance Goals**: N/A — this change only affects retry sleep duration
|
|
34
|
+
|
|
35
|
+
**Constraints**: All 65 existing unit tests and 6 integration tests must pass; `mypy --strict` must pass
|
|
36
|
+
|
|
37
|
+
**Scale/Scope**: Small, targeted removal — 7 files touched, 2 tests deleted, no new files
|
|
38
|
+
|
|
39
|
+
## Constitution Check
|
|
40
|
+
|
|
41
|
+
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
|
42
|
+
|
|
43
|
+
- [X] Validates Provider Pattern Architecture — no changes to `BaseExtractor` ABC or provider pattern
|
|
44
|
+
- [X] Confirms Technology Stack — no dependency additions or removals
|
|
45
|
+
- [X] Adheres to Coding Standards — PEP 8, strict type hints; removing code only improves compliance
|
|
46
|
+
- [X] Incorporates Integration Testing — integration tests remain; fixtures simplified not removed
|
|
47
|
+
- [X] Respects Packaging and Distribution standards — `pyproject.toml`, `src/` layout, `uv` unchanged
|
|
48
|
+
|
|
49
|
+
**No violations.** Complexity Tracking table not required.
|
|
50
|
+
|
|
51
|
+
## Project Structure
|
|
52
|
+
|
|
53
|
+
### Documentation (this feature)
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
specs/004-remove-backoff/
|
|
57
|
+
├── plan.md ← this file
|
|
58
|
+
├── research.md ← Phase 0 output
|
|
59
|
+
└── tasks.md ← Phase 2 output (/speckit-tasks)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Source Code (files changed by this feature)
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
src/mdfetch/
|
|
66
|
+
├── base.py # remove _MAX_RETRY_DELAY; fix sleep formula; update docstring
|
|
67
|
+
└── __init__.py # update docstring (remove exponential reference)
|
|
68
|
+
|
|
69
|
+
tests/
|
|
70
|
+
├── unit/test_fetch_errors.py # remove 2 backoff tests; update 2 stale "backoff" docstrings; add sleep-value assertion to test_status_code_not_in_no_retry_set_still_retries
|
|
71
|
+
└── integration/
|
|
72
|
+
├── conftest.py # remove os import + env var reads; simplify/remove fixtures
|
|
73
|
+
├── test_medium_integration.py # remove fixture params; inline retries=3, retry_delay=2.0
|
|
74
|
+
└── test_devto_integration.py # remove fixture params; inline retries=3, retry_delay=2.0
|
|
75
|
+
|
|
76
|
+
.github/workflows/integration.yml # remove MDFETCH_RETRIES and MDFETCH_RETRY_DELAY env vars
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Phase 0: Research
|
|
80
|
+
|
|
81
|
+
No NEEDS CLARIFICATION items and no new external dependencies. All design decisions are determined by the existing codebase.
|
|
82
|
+
|
|
83
|
+
See [research.md](research.md) for the decision log.
|
|
84
|
+
|
|
85
|
+
## Phase 1: Design
|
|
86
|
+
|
|
87
|
+
### Data Model
|
|
88
|
+
|
|
89
|
+
No entities. This feature removes code; no new data structures are introduced.
|
|
90
|
+
|
|
91
|
+
### Contracts
|
|
92
|
+
|
|
93
|
+
The public `extract()` signature and exception contract are **unchanged**:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
def extract(url: str, *, retries: int = 3, retry_delay: float = 2.0) -> str
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- Same parameters, same defaults, same return type, same exception hierarchy.
|
|
100
|
+
- The only observable behaviour change: the sleep between retries is now a flat `retry_delay` seconds rather than `retry_delay × 2ⁿ`. This is not part of the public API contract.
|
|
101
|
+
|
|
102
|
+
No `contracts/` directory needed.
|
|
103
|
+
|
|
104
|
+
### Integration Test Strategy
|
|
105
|
+
|
|
106
|
+
**Before** (PR #6 pattern):
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# conftest.py reads env vars
|
|
110
|
+
@pytest.fixture
|
|
111
|
+
def http_retries() -> int:
|
|
112
|
+
return int(os.environ.get("MDFETCH_RETRIES", "3"))
|
|
113
|
+
|
|
114
|
+
# test uses fixture params
|
|
115
|
+
def test_extract_contains_snapshot(
|
|
116
|
+
url: str, snapshot: str, http_retries: int, http_retry_delay: float
|
|
117
|
+
) -> None:
|
|
118
|
+
result = extract(url, retries=http_retries, retry_delay=http_retry_delay)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**After** (this feature):
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# conftest.py deleted or left empty
|
|
125
|
+
# test uses hardcoded constants
|
|
126
|
+
def test_extract_contains_snapshot(url: str, snapshot: str) -> None:
|
|
127
|
+
result = extract(url, retries=3, retry_delay=2.0)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Rationale: the fixtures existed solely to expose env-var configurability. Without that purpose they are dead abstraction. Inlining `3` and `2.0` is simpler and leaves no misleading indirection.
|
|
131
|
+
|
|
132
|
+
### Key Changes by File
|
|
133
|
+
|
|
134
|
+
| File | Change |
|
|
135
|
+
|------|--------|
|
|
136
|
+
| `src/mdfetch/base.py` | Remove `_MAX_RETRY_DELAY = 60.0`; change `time.sleep(min(_MAX_RETRY_DELAY, retry_delay * (2**attempt)))` → `time.sleep(retry_delay)`; update docstring |
|
|
137
|
+
| `src/mdfetch/__init__.py` | Update docstring: replace "exponential backoff starting at *retry_delay* seconds" |
|
|
138
|
+
| `tests/unit/test_fetch_errors.py` | Delete `test_exponential_backoff_sleep_sequence` and `test_exponential_backoff_capped_at_max_delay`; update 2 stale "backoff" docstrings; add sleep-value assertion to `test_status_code_not_in_no_retry_set_still_retries` |
|
|
139
|
+
| `tests/integration/conftest.py` | Remove `import os`, remove both fixtures (or delete file if empty) |
|
|
140
|
+
| `tests/integration/test_medium_integration.py` | Remove `http_retries`/`http_retry_delay` fixture params; inline `retries=3, retry_delay=2.0` |
|
|
141
|
+
| `tests/integration/test_devto_integration.py` | Same as above |
|
|
142
|
+
| `.github/workflows/integration.yml` | Remove `MDFETCH_RETRIES: "6"` and `MDFETCH_RETRY_DELAY: "2.0"` from `env:` block |
|
|
143
|
+
|
|
144
|
+
### Revision: Implementation Sync 2026-05-15
|
|
145
|
+
- Two stale "backoff" docstrings in `tests/unit/test_fetch_errors.py` were not captured in the original plan; added to key-changes table.
|
|
146
|
+
- `test_status_code_not_in_no_retry_set_still_retries` lacked a sleep-value assertion; added to key-changes table to close the AC1 gap.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Research: Remove Exponential Backoff and Env-Var Retry Config
|
|
2
|
+
|
|
3
|
+
**Feature**: `004-remove-backoff` | **Date**: 2026-05-15
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
No external research required. All decisions are determined by the existing codebase and the motivation documented in the spec.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Decision 1: Fixed vs Exponential Delay
|
|
12
|
+
|
|
13
|
+
**Decision**: Replace exponential backoff with a fixed `retry_delay` seconds between attempts.
|
|
14
|
+
|
|
15
|
+
**Rationale**: The exponential schedule was introduced to work around aggressive rate-limiting from medium.com in CI. That problem is now solved at a higher level by the Freedium fallback (PR #7): 403/429 responses trigger an immediate switch to Freedium rather than retrying against medium.com at all. The exponential schedule therefore provides no additional resilience but adds complexity and unpredictability to retry timing.
|
|
16
|
+
|
|
17
|
+
**Alternatives considered**:
|
|
18
|
+
- Keep exponential, remove only env vars → Rejected. Removes the tooling noise but keeps the unnecessary complexity in the core retry loop.
|
|
19
|
+
- Keep exponential, add deprecation warning → Rejected. A deprecation period is unnecessary for an internal mechanism not part of the public API.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Decision 2: Remove Fixtures vs Hardcode Defaults
|
|
24
|
+
|
|
25
|
+
**Decision**: Delete the `http_retries` and `http_retry_delay` pytest fixtures from `conftest.py` and inline `retries=3, retry_delay=2.0` directly in integration test calls.
|
|
26
|
+
|
|
27
|
+
**Rationale**: The fixtures existed as thin wrappers around env-var reads. Once the env vars are removed, the fixtures become single-line constants with no abstraction value. Inlining the defaults removes a layer of indirection that would otherwise require readers to trace through `conftest.py` to understand what values are used.
|
|
28
|
+
|
|
29
|
+
**Alternatives considered**:
|
|
30
|
+
- Keep fixtures with hardcoded defaults → Rejected. A fixture returning a literal integer is dead abstraction; it signals configurability that doesn't exist.
|
|
31
|
+
- Move constants to a `conftest.py` module-level variable → Rejected. Same problem — adds indirection without benefit.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Decision 3: Handling `conftest.py` After Fixture Removal
|
|
36
|
+
|
|
37
|
+
**Decision**: Delete `conftest.py` entirely since removing both fixtures leaves the file empty (only `import os` and two functions, all of which are removed).
|
|
38
|
+
|
|
39
|
+
**Rationale**: An empty `conftest.py` is harmless but misleading — it implies there is shared test configuration when there is none. Deleting it is cleaner.
|
|
40
|
+
|
|
41
|
+
**Alternatives considered**:
|
|
42
|
+
- Leave empty file → Rejected. No benefit; creates noise in the file tree.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Feature Specification: Remove Exponential Backoff and Env-Var Retry Config
|
|
2
|
+
|
|
3
|
+
**Feature Branch**: `004-remove-backoff`
|
|
4
|
+
|
|
5
|
+
**Created**: 2026-05-15
|
|
6
|
+
|
|
7
|
+
**Status**: Completed
|
|
8
|
+
|
|
9
|
+
**Input**: User description: "let's deprecate and remove the exponential backoff and env.variables which was implemented in https://github.com/stn1slv/md-fetch/pull/6"
|
|
10
|
+
|
|
11
|
+
## Background
|
|
12
|
+
|
|
13
|
+
PR #6 introduced two related changes to mdfetch:
|
|
14
|
+
|
|
15
|
+
1. **Exponential backoff** — the wait before retry attempt *n* was changed from a fixed `retry_delay` seconds to `min(60, retry_delay × 2ⁿ)`. A `_MAX_RETRY_DELAY = 60.0` module-level constant was added to cap the delay.
|
|
16
|
+
2. **Configurable retries via environment variables** — `tests/integration/conftest.py` reads `MDFETCH_RETRIES` and `MDFETCH_RETRY_DELAY` env vars and exposes them as pytest fixtures. CI sets `MDFETCH_RETRIES=6` in `integration.yml`.
|
|
17
|
+
|
|
18
|
+
Both changes are to be removed, restoring the simpler fixed-delay retry behaviour.
|
|
19
|
+
|
|
20
|
+
## User Scenarios & Testing *(mandatory)*
|
|
21
|
+
|
|
22
|
+
### User Story 1 - Fixed-Delay Retry Behaviour Restored (Priority: P1)
|
|
23
|
+
|
|
24
|
+
A developer calling `extract()` encounters a transient network error. The library retries using a simple, predictable fixed delay — exactly `retry_delay` seconds between every attempt — rather than an exponentially growing delay.
|
|
25
|
+
|
|
26
|
+
**Why this priority**: This is the core change. Simple, deterministic retry behaviour is easier to reason about and test. The exponential schedule was added to work around aggressive CI rate-limiting; that problem is now solved at the infrastructure level (Freedium fallback for 403/429) rather than in the retry loop.
|
|
27
|
+
|
|
28
|
+
**Independent Test**: Can be fully tested by calling `fetch_html` with a failing URL and verifying that `time.sleep` is called with the exact `retry_delay` value for every inter-attempt gap (not an exponentially growing value).
|
|
29
|
+
|
|
30
|
+
**Acceptance Scenarios**:
|
|
31
|
+
|
|
32
|
+
1. **Given** `fetch_html` is called with `retries=3` and `retry_delay=2.0`, **When** the first two attempts raise a transient error, **Then** the library sleeps exactly `2.0` seconds before each retry (not `2.0` then `4.0`).
|
|
33
|
+
2. **Given** `fetch_html` is called with `retries=1`, **When** the attempt fails, **Then** no sleep occurs and the exception is raised immediately.
|
|
34
|
+
3. **Given** a status code that is in `_no_retry_status_codes`, **When** that error is raised, **Then** no sleep or retry occurs.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### User Story 2 - Integration Tests Use Hardcoded Retry Defaults (Priority: P2)
|
|
39
|
+
|
|
40
|
+
A developer runs `make integration` locally or in CI without setting any `MDFETCH_*` environment variables. The integration tests use fixed defaults (3 retries, 2.0 s delay) and do not consult environment variables.
|
|
41
|
+
|
|
42
|
+
**Why this priority**: Env vars coupling test behaviour to CI configuration makes tests harder to reason about and reproduce locally. With the Freedium fallback absorbing 403/429 errors, 3 retries at a fixed 2 s delay is sufficient.
|
|
43
|
+
|
|
44
|
+
**Independent Test**: Can be fully tested by running `make integration` without setting `MDFETCH_RETRIES` or `MDFETCH_RETRY_DELAY` and verifying all 6 tests pass.
|
|
45
|
+
|
|
46
|
+
**Acceptance Scenarios**:
|
|
47
|
+
|
|
48
|
+
1. **Given** neither `MDFETCH_RETRIES` nor `MDFETCH_RETRY_DELAY` is set, **When** `make integration` runs, **Then** all integration tests pass using 3 retries and 2.0 s delay.
|
|
49
|
+
2. **Given** the `MDFETCH_RETRIES=6` env var is set in `integration.yml`, **When** this feature is shipped, **Then** that env var is removed from the CI workflow.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### Edge Cases
|
|
54
|
+
|
|
55
|
+
- What happens if tests currently rely on the `http_retries` / `http_retry_delay` fixtures by name? They must be updated to use hardcoded defaults or inline constants.
|
|
56
|
+
- What happens to the two unit tests that assert exponential sleep sequences? They must be removed entirely.
|
|
57
|
+
- What happens to the `_MAX_RETRY_DELAY` module-level constant? It is unused after the change and must be removed to avoid dead code.
|
|
58
|
+
|
|
59
|
+
## Requirements *(mandatory)*
|
|
60
|
+
|
|
61
|
+
### Functional Requirements
|
|
62
|
+
|
|
63
|
+
- **FR-001**: The `fetch_html` method MUST use a fixed delay of exactly `retry_delay` seconds between retry attempts — no exponential multiplication.
|
|
64
|
+
- **FR-002**: The `_MAX_RETRY_DELAY` module-level constant in `base.py` MUST be removed.
|
|
65
|
+
- **FR-003**: The `import time` statement in `base.py` MUST be retained (still needed for `time.sleep`).
|
|
66
|
+
- **FR-004**: The `MDFETCH_RETRIES` and `MDFETCH_RETRY_DELAY` environment variable reads in `tests/integration/conftest.py` MUST be removed.
|
|
67
|
+
- **FR-005**: The `http_retries` and `http_retry_delay` pytest fixtures MUST be simplified to return hardcoded defaults (`3` and `2.0` respectively) or be inlined into the test functions.
|
|
68
|
+
- **FR-006**: The `MDFETCH_RETRIES` and `MDFETCH_RETRY_DELAY` env var entries in `.github/workflows/integration.yml` MUST be removed.
|
|
69
|
+
- **FR-007**: The two unit tests `test_exponential_backoff_sleep_sequence` and `test_exponential_backoff_capped_at_max_delay` in `tests/unit/test_fetch_errors.py` MUST be removed.
|
|
70
|
+
- **FR-008**: The `fetch_html` docstring in `base.py` MUST be updated to describe fixed-delay retry behaviour (remove references to exponential schedule and `min(60, …)` formula).
|
|
71
|
+
- **FR-009**: The `extract()` docstring in `src/mdfetch/__init__.py` MUST be updated if it references exponential backoff.
|
|
72
|
+
- **FR-010**: All remaining unit and integration tests MUST continue to pass after the changes.
|
|
73
|
+
|
|
74
|
+
## Success Criteria *(mandatory)*
|
|
75
|
+
|
|
76
|
+
### Measurable Outcomes
|
|
77
|
+
|
|
78
|
+
- **SC-001**: `make test` passes with zero failures after the changes (all unit tests green).
|
|
79
|
+
- **SC-002**: `make integration` passes with zero failures when run without any `MDFETCH_*` environment variables.
|
|
80
|
+
- **SC-003**: The two exponential-backoff unit tests no longer exist in the test suite.
|
|
81
|
+
- **SC-004**: No reference to `MDFETCH_RETRIES`, `MDFETCH_RETRY_DELAY`, or `_MAX_RETRY_DELAY` remains in `src/`, `tests/`, or `.github/` (spec and documentation files are excluded).
|
|
82
|
+
- **SC-005**: The CI `integration.yml` workflow no longer sets `MDFETCH_RETRIES` or `MDFETCH_RETRY_DELAY`.
|
|
83
|
+
|
|
84
|
+
## Assumptions
|
|
85
|
+
|
|
86
|
+
- The Freedium fallback (introduced in PR #7) adequately handles the Medium 403/429 rate-limiting that originally motivated exponential backoff. Fixed delay at 3 retries is therefore sufficient for CI reliability.
|
|
87
|
+
- The `http_retries` and `http_retry_delay` fixtures in `conftest.py` are used only in `test_medium_integration.py` and `test_devto_integration.py`; no other test files depend on them.
|
|
88
|
+
- The `timeout-minutes: 30` setting in `integration.yml` can remain as-is — it provides adequate headroom even with fixed delay retries.
|
|
89
|
+
|
|
90
|
+
### Revision: Implementation Sync 2026-05-15
|
|
91
|
+
- Stale "backoff" terminology found in two docstrings in `tests/unit/test_fetch_errors.py` (lines 98, 123); these are test comments not covered by FR-004 or FR-007 and require a targeted cleanup task.
|
|
92
|
+
- `test_status_code_not_in_no_retry_set_still_retries` verifies sleep count but not sleep value; US1 AC1 ("sleeps exactly `retry_delay` seconds") implies a value assertion is also needed.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Tasks: Remove Exponential Backoff and Env-Var Retry Config
|
|
2
|
+
|
|
3
|
+
**Input**: Design documents from `specs/004-remove-backoff/`
|
|
4
|
+
|
|
5
|
+
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅
|
|
6
|
+
|
|
7
|
+
**Organization**: Tasks are grouped by user story to enable independent implementation and testing.
|
|
8
|
+
|
|
9
|
+
## Format: `[ID] [P?] [Story] Description`
|
|
10
|
+
|
|
11
|
+
- **[P]**: Can run in parallel (different files, no dependencies on incomplete tasks)
|
|
12
|
+
- **[Story]**: Which user story this task belongs to (US1, US2)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Phase 1: Setup
|
|
17
|
+
|
|
18
|
+
**Purpose**: Establish green baseline before making any changes
|
|
19
|
+
|
|
20
|
+
- [X] T001 Verify baseline by running `make test` — confirm 65 unit tests pass before changes
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Phase 2: Foundational (Blocking Prerequisites)
|
|
25
|
+
|
|
26
|
+
**Purpose**: No shared infrastructure changes needed for this feature — this phase is satisfied by T001.
|
|
27
|
+
|
|
28
|
+
**⚠️ NOTE**: US1 and US2 are fully independent; either can be implemented first or in parallel.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Phase 3: User Story 1 — Fixed-Delay Retry Behaviour Restored (Priority: P1) 🎯 MVP
|
|
33
|
+
|
|
34
|
+
**Goal**: Replace the exponential backoff formula with a flat `retry_delay` seconds sleep; remove the `_MAX_RETRY_DELAY` cap constant; update docstrings; delete the two unit tests that verified the exponential schedule.
|
|
35
|
+
|
|
36
|
+
**Independent Test**: Run `make test` — the remaining retry tests (`TestNoRetryStatusCodes`, `TestFetchErrors`) must pass; the two deleted tests must no longer exist.
|
|
37
|
+
|
|
38
|
+
### Implementation for User Story 1
|
|
39
|
+
|
|
40
|
+
- [X] T002 [US1] Remove `_MAX_RETRY_DELAY = 60.0` constant and change `time.sleep(min(_MAX_RETRY_DELAY, retry_delay * (2**attempt)))` to `time.sleep(retry_delay)` in `src/mdfetch/base.py`
|
|
41
|
+
- [X] T003 [US1] Update `fetch_html` docstring in `src/mdfetch/base.py` to say "fixed delay of *retry_delay* seconds" (remove mention of exponential schedule and `min(60, …)` formula)
|
|
42
|
+
- [X] T004 [P] [US1] Update `extract()` docstring in `src/mdfetch/__init__.py` — replace "exponential backoff starting at *retry_delay* seconds" with "fixed delay of *retry_delay* seconds between attempts"
|
|
43
|
+
- [X] T005 [P] [US1] Delete `test_exponential_backoff_sleep_sequence` and `test_exponential_backoff_capped_at_max_delay` test methods from `tests/unit/test_fetch_errors.py`
|
|
44
|
+
|
|
45
|
+
**Checkpoint**: Run `make test` — all remaining unit tests pass; no reference to exponential backoff remains in `src/`.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Phase 4: User Story 2 — Integration Tests Use Hardcoded Retry Defaults (Priority: P2)
|
|
50
|
+
|
|
51
|
+
**Goal**: Remove the `MDFETCH_RETRIES` / `MDFETCH_RETRY_DELAY` env-var fixtures from the integration test suite and CI; inline the defaults directly in test call sites; delete `conftest.py`.
|
|
52
|
+
|
|
53
|
+
**Independent Test**: Run `make integration` without any `MDFETCH_*` env vars set — all 6 integration tests pass.
|
|
54
|
+
|
|
55
|
+
### Implementation for User Story 2
|
|
56
|
+
|
|
57
|
+
- [X] T006 [P] [US2] Remove `http_retries` and `http_retry_delay` fixture parameters from the test function signature and inline `retries=3, retry_delay=2.0` in the `extract()` call in `tests/integration/test_medium_integration.py`; remove the env-var docstring line from the test docstring
|
|
58
|
+
- [X] T007 [P] [US2] Same changes as T006 in `tests/integration/test_devto_integration.py`
|
|
59
|
+
- [X] T008 [US2] Delete `tests/integration/conftest.py` (file is empty after T006 + T007 remove all fixture consumers; no other test files reference these fixtures)
|
|
60
|
+
- [X] T009 [US2] Remove the `MDFETCH_RETRIES: "6"` and `MDFETCH_RETRY_DELAY: "2.0"` lines from the `env:` block of the `Run integration tests` step in `.github/workflows/integration.yml`
|
|
61
|
+
|
|
62
|
+
**Checkpoint**: Run `make integration` — all 6 tests pass; no `MDFETCH_*` variables appear anywhere in the codebase.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Phase 5: Polish & Cross-Cutting Concerns
|
|
67
|
+
|
|
68
|
+
**Purpose**: Final validation — confirm all quality gates pass together
|
|
69
|
+
|
|
70
|
+
- [X] T010 Run `make test` to confirm all unit tests pass (expected: 63 tests — 65 minus the 2 deleted backoff tests)
|
|
71
|
+
- [X] T011 Run `uv run mypy src/` to confirm zero type errors after docstring and code changes
|
|
72
|
+
- [X] T012 Run `make lint` to confirm ruff check passes with no violations
|
|
73
|
+
- [X] T013 Verify no remaining references to `_MAX_RETRY_DELAY`, `MDFETCH_RETRIES`, or `MDFETCH_RETRY_DELAY` in source, tests, and CI by running `grep -r "MAX_RETRY_DELAY\|MDFETCH_RETRIES\|MDFETCH_RETRY_DELAY" src/ tests/ .github/` (spec/doc files are intentionally excluded)
|
|
74
|
+
- [X] T014 Run `make integration` without any `MDFETCH_*` env vars set and confirm all 6 integration tests pass (verifies SC-002)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Remediation: Gaps
|
|
79
|
+
|
|
80
|
+
*Added by Implementation Sync 2026-05-15*
|
|
81
|
+
|
|
82
|
+
- [X] T015 [P] [US1] Update stale "backoff" docstrings in `tests/unit/test_fetch_errors.py`: line 98 "no backoff sleep" → "no retry sleep"; line 123 "trigger backoff" → "trigger retry with fixed delay" [Sync: Gap Report]
|
|
83
|
+
- [X] T016 [US1] Add sleep-value assertion to `test_status_code_not_in_no_retry_set_still_retries` in `tests/unit/test_fetch_errors.py` — add `assert mock_sleep.call_args_list == [call(1.0), call(1.0)]` after the call_count check (verifies US1 AC1: "sleeps exactly retry_delay seconds") [Sync: Gap Report]
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Dependencies & Execution Order
|
|
88
|
+
|
|
89
|
+
### Phase Dependencies
|
|
90
|
+
|
|
91
|
+
- **Setup (Phase 1)**: No dependencies — start immediately
|
|
92
|
+
- **US1 (Phase 3)**: Depends on T001 (baseline green) — can start after setup
|
|
93
|
+
- **US2 (Phase 4)**: Depends on T001 (baseline green) — **independent from US1; can run in parallel**
|
|
94
|
+
- **Polish (Phase 5)**: Depends on US1 and US2 both complete
|
|
95
|
+
|
|
96
|
+
### Within Each User Story
|
|
97
|
+
|
|
98
|
+
- **US1**: T002 → T003 (same file, sequential); T004 and T005 are [P] (different files, independent of T002/T003)
|
|
99
|
+
- **US2**: T006 and T007 are [P] (different files); T008 depends on T006 + T007 (delete conftest only after test files no longer reference fixtures); T009 is [P] with T006/T007/T008 (different file)
|
|
100
|
+
|
|
101
|
+
### Parallel Opportunities
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
After T001 (baseline green):
|
|
105
|
+
|
|
106
|
+
Stream A (US1):
|
|
107
|
+
T002 → T003 # base.py: formula + docstring (same file, sequential)
|
|
108
|
+
T004 # __init__.py docstring [P with T005]
|
|
109
|
+
T005 # delete backoff unit tests [P with T004]
|
|
110
|
+
|
|
111
|
+
Stream B (US2):
|
|
112
|
+
T006 [P] # test_medium_integration.py (parallel with T007)
|
|
113
|
+
T007 [P] # test_devto_integration.py (parallel with T006)
|
|
114
|
+
T008 # delete conftest.py (after T006 + T007)
|
|
115
|
+
T009 # integration.yml env vars [P, independent]
|
|
116
|
+
|
|
117
|
+
After both streams: T010 → T011 → T012 → T013 → T014
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Implementation Strategy
|
|
123
|
+
|
|
124
|
+
### MVP First (User Story 1 Only)
|
|
125
|
+
|
|
126
|
+
1. T001 — establish baseline
|
|
127
|
+
2. T002 → T003 — fix retry formula and docstring in `base.py`
|
|
128
|
+
3. T004 + T005 — update `__init__.py` docstring; delete backoff tests
|
|
129
|
+
4. T010 + T011 + T012 — validate
|
|
130
|
+
5. **STOP and VALIDATE**: `make test` passes with 63 tests
|
|
131
|
+
|
|
132
|
+
### Full Delivery (US1 + US2)
|
|
133
|
+
|
|
134
|
+
Continue after MVP:
|
|
135
|
+
6. T006 + T007 — update integration test signatures
|
|
136
|
+
7. T008 — delete conftest.py
|
|
137
|
+
8. T009 — remove CI env vars
|
|
138
|
+
9. T013 — confirm no residual references
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Notes
|
|
143
|
+
|
|
144
|
+
- [P] tasks operate on different files with no shared dependencies — safe to run concurrently
|
|
145
|
+
- T008 (delete conftest.py) MUST come after T006 + T007 — deleting conftest.py while test functions still declare `http_retries`/`http_retry_delay` parameters will cause pytest fixture-not-found errors
|
|
146
|
+
- T009 (.github/workflows) is independent of all other tasks; can be done at any point after T001
|
|
147
|
+
- Expected final unit test count: 63 (65 baseline − 2 deleted backoff tests)
|