digest-generator 0.1.0__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.
Files changed (148) hide show
  1. digest_generator-0.1.0/.env.example +131 -0
  2. digest_generator-0.1.0/.github/CODEOWNERS +1 -0
  3. digest_generator-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  4. digest_generator-0.1.0/.github/ISSUE_TEMPLATE/config.yml +6 -0
  5. digest_generator-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  6. digest_generator-0.1.0/.github/pull_request_template.md +24 -0
  7. digest_generator-0.1.0/.github/workflows/ci.yml +38 -0
  8. digest_generator-0.1.0/.github/workflows/release.yml +103 -0
  9. digest_generator-0.1.0/.gitignore +63 -0
  10. digest_generator-0.1.0/.gitleaks.toml +23 -0
  11. digest_generator-0.1.0/.pre-commit-config.yaml +81 -0
  12. digest_generator-0.1.0/.python-version +1 -0
  13. digest_generator-0.1.0/.secrets.baseline +133 -0
  14. digest_generator-0.1.0/CHANGELOG.md +25 -0
  15. digest_generator-0.1.0/CODE_OF_CONDUCT.md +35 -0
  16. digest_generator-0.1.0/CONTRIBUTING.md +92 -0
  17. digest_generator-0.1.0/LICENSE.md +202 -0
  18. digest_generator-0.1.0/NOTICE.md +7 -0
  19. digest_generator-0.1.0/PKG-INFO +148 -0
  20. digest_generator-0.1.0/README.md +102 -0
  21. digest_generator-0.1.0/SECURITY.md +36 -0
  22. digest_generator-0.1.0/digest_generator/__init__.py +0 -0
  23. digest_generator-0.1.0/digest_generator/api.py +631 -0
  24. digest_generator-0.1.0/digest_generator/cli.py +907 -0
  25. digest_generator-0.1.0/digest_generator/core/__init__.py +0 -0
  26. digest_generator-0.1.0/digest_generator/core/audio/__init__.py +12 -0
  27. digest_generator-0.1.0/digest_generator/core/audio/io.py +128 -0
  28. digest_generator-0.1.0/digest_generator/core/audio/narration.py +347 -0
  29. digest_generator-0.1.0/digest_generator/core/audio/narration_overrides.yaml +210 -0
  30. digest_generator-0.1.0/digest_generator/core/audio/renderer.py +156 -0
  31. digest_generator-0.1.0/digest_generator/core/audio/types.py +50 -0
  32. digest_generator-0.1.0/digest_generator/core/categories.py +91 -0
  33. digest_generator-0.1.0/digest_generator/core/digest/__init__.py +4 -0
  34. digest_generator-0.1.0/digest_generator/core/digest/io.py +255 -0
  35. digest_generator-0.1.0/digest_generator/core/digest/orchestrator.py +519 -0
  36. digest_generator-0.1.0/digest_generator/core/digest/prompts/__init__.py +23 -0
  37. digest_generator-0.1.0/digest_generator/core/digest/prompts/templates/cluster_system.md +73 -0
  38. digest_generator-0.1.0/digest_generator/core/digest/prompts/templates/editorial_pass_system.md +80 -0
  39. digest_generator-0.1.0/digest_generator/core/digest/prompts/templates/intro_system.md +70 -0
  40. digest_generator-0.1.0/digest_generator/core/digest/prompts/templates/section_merge_system.md +53 -0
  41. digest_generator-0.1.0/digest_generator/core/digest/prompts/templates/section_system.md +80 -0
  42. digest_generator-0.1.0/digest_generator/core/digest/prompts/templates/title_system.md +38 -0
  43. digest_generator-0.1.0/digest_generator/core/digest/prompts/templates/watch_system.md +71 -0
  44. digest_generator-0.1.0/digest_generator/core/digest/stages/__init__.py +0 -0
  45. digest_generator-0.1.0/digest_generator/core/digest/stages/clusterer.py +513 -0
  46. digest_generator-0.1.0/digest_generator/core/digest/stages/composer.py +111 -0
  47. digest_generator-0.1.0/digest_generator/core/digest/stages/editorial.py +233 -0
  48. digest_generator-0.1.0/digest_generator/core/digest/stages/framer.py +256 -0
  49. digest_generator-0.1.0/digest_generator/core/digest/stages/watcher.py +237 -0
  50. digest_generator-0.1.0/digest_generator/core/digest/stages/writer.py +375 -0
  51. digest_generator-0.1.0/digest_generator/core/digest/types.py +94 -0
  52. digest_generator-0.1.0/digest_generator/core/label/__init__.py +16 -0
  53. digest_generator-0.1.0/digest_generator/core/label/io.py +140 -0
  54. digest_generator-0.1.0/digest_generator/core/label/stages/__init__.py +8 -0
  55. digest_generator-0.1.0/digest_generator/core/label/stages/topic.py +174 -0
  56. digest_generator-0.1.0/digest_generator/core/prompt_loader.py +58 -0
  57. digest_generator-0.1.0/digest_generator/core/style.py +187 -0
  58. digest_generator-0.1.0/digest_generator/core/summary/__init__.py +11 -0
  59. digest_generator-0.1.0/digest_generator/core/summary/io.py +123 -0
  60. digest_generator-0.1.0/digest_generator/core/summary/prompts/__init__.py +25 -0
  61. digest_generator-0.1.0/digest_generator/core/summary/prompts/templates/article_summary_system.md +38 -0
  62. digest_generator-0.1.0/digest_generator/core/summary/stages/__init__.py +8 -0
  63. digest_generator-0.1.0/digest_generator/core/summary/stages/summarizer.py +175 -0
  64. digest_generator-0.1.0/digest_generator/core/types.py +205 -0
  65. digest_generator-0.1.0/digest_generator/py.typed +0 -0
  66. digest_generator-0.1.0/digest_generator/shared/__init__.py +0 -0
  67. digest_generator-0.1.0/digest_generator/shared/hf_hub.py +31 -0
  68. digest_generator-0.1.0/digest_generator/shared/llm/__init__.py +1 -0
  69. digest_generator-0.1.0/digest_generator/shared/llm/clients.py +83 -0
  70. digest_generator-0.1.0/digest_generator/shared/llm/sampling.py +256 -0
  71. digest_generator-0.1.0/digest_generator/shared/llm/telemetry.py +317 -0
  72. digest_generator-0.1.0/digest_generator/shared/llm/typography.py +27 -0
  73. digest_generator-0.1.0/digest_generator/shared/logging.py +403 -0
  74. digest_generator-0.1.0/digest_generator/shared/runtime/__init__.py +18 -0
  75. digest_generator-0.1.0/digest_generator/shared/runtime/dirs.py +45 -0
  76. digest_generator-0.1.0/digest_generator/shared/runtime/meta.py +349 -0
  77. digest_generator-0.1.0/digest_generator/shared/settings.py +205 -0
  78. digest_generator-0.1.0/digest_generator/shared/transformers/__init__.py +22 -0
  79. digest_generator-0.1.0/digest_generator/shared/transformers/registry.py +40 -0
  80. digest_generator-0.1.0/digest_generator/shared/transformers/types.py +84 -0
  81. digest_generator-0.1.0/digest_generator/shared/tts/__init__.py +19 -0
  82. digest_generator-0.1.0/digest_generator/shared/tts/engine.py +233 -0
  83. digest_generator-0.1.0/digest_generator/shared/tts/registry.py +107 -0
  84. digest_generator-0.1.0/digest_generator/shared/tts/types.py +39 -0
  85. digest_generator-0.1.0/digest_generator/sources/__init__.py +9 -0
  86. digest_generator-0.1.0/digest_generator/sources/rss/config.py +267 -0
  87. digest_generator-0.1.0/digest_generator/sources/rss/fetcher.py +263 -0
  88. digest_generator-0.1.0/digest_generator/sources/rss/io.py +154 -0
  89. digest_generator-0.1.0/digest_generator/sources/rss/types.py +71 -0
  90. digest_generator-0.1.0/docs/setup.md +152 -0
  91. digest_generator-0.1.0/docs/usage.md +338 -0
  92. digest_generator-0.1.0/feeds.example.yaml +41 -0
  93. digest_generator-0.1.0/pyproject.toml +230 -0
  94. digest_generator-0.1.0/scripts/test_digest.py +25 -0
  95. digest_generator-0.1.0/tests/__init__.py +0 -0
  96. digest_generator-0.1.0/tests/conftest.py +44 -0
  97. digest_generator-0.1.0/tests/core/__init__.py +0 -0
  98. digest_generator-0.1.0/tests/core/audio/__init__.py +0 -0
  99. digest_generator-0.1.0/tests/core/audio/test_io.py +135 -0
  100. digest_generator-0.1.0/tests/core/audio/test_narration.py +481 -0
  101. digest_generator-0.1.0/tests/core/audio/test_renderer.py +252 -0
  102. digest_generator-0.1.0/tests/core/audio/test_types.py +52 -0
  103. digest_generator-0.1.0/tests/core/digest/__init__.py +0 -0
  104. digest_generator-0.1.0/tests/core/digest/test_io.py +438 -0
  105. digest_generator-0.1.0/tests/core/digest/test_types.py +59 -0
  106. digest_generator-0.1.0/tests/core/label/__init__.py +0 -0
  107. digest_generator-0.1.0/tests/core/label/test_io.py +144 -0
  108. digest_generator-0.1.0/tests/core/summary/__init__.py +0 -0
  109. digest_generator-0.1.0/tests/core/summary/test_io.py +184 -0
  110. digest_generator-0.1.0/tests/core/test_categories.py +106 -0
  111. digest_generator-0.1.0/tests/core/test_clusterer.py +452 -0
  112. digest_generator-0.1.0/tests/core/test_composer.py +180 -0
  113. digest_generator-0.1.0/tests/core/test_digest_style.py +208 -0
  114. digest_generator-0.1.0/tests/core/test_editorial.py +391 -0
  115. digest_generator-0.1.0/tests/core/test_framer.py +339 -0
  116. digest_generator-0.1.0/tests/core/test_prompt_loader.py +90 -0
  117. digest_generator-0.1.0/tests/core/test_summarizer.py +269 -0
  118. digest_generator-0.1.0/tests/core/test_topic.py +206 -0
  119. digest_generator-0.1.0/tests/core/test_types.py +221 -0
  120. digest_generator-0.1.0/tests/core/test_watcher.py +289 -0
  121. digest_generator-0.1.0/tests/core/test_writer.py +650 -0
  122. digest_generator-0.1.0/tests/shared/__init__.py +0 -0
  123. digest_generator-0.1.0/tests/shared/llm/__init__.py +0 -0
  124. digest_generator-0.1.0/tests/shared/llm/test_clients.py +172 -0
  125. digest_generator-0.1.0/tests/shared/llm/test_sampling.py +318 -0
  126. digest_generator-0.1.0/tests/shared/llm/test_telemetry.py +493 -0
  127. digest_generator-0.1.0/tests/shared/llm/test_typography.py +25 -0
  128. digest_generator-0.1.0/tests/shared/runtime/__init__.py +0 -0
  129. digest_generator-0.1.0/tests/shared/runtime/test_dirs.py +46 -0
  130. digest_generator-0.1.0/tests/shared/runtime/test_meta.py +490 -0
  131. digest_generator-0.1.0/tests/shared/test_hf_hub.py +24 -0
  132. digest_generator-0.1.0/tests/shared/test_logging.py +416 -0
  133. digest_generator-0.1.0/tests/shared/test_settings.py +137 -0
  134. digest_generator-0.1.0/tests/shared/transformers/__init__.py +0 -0
  135. digest_generator-0.1.0/tests/shared/transformers/test_types.py +61 -0
  136. digest_generator-0.1.0/tests/shared/tts/__init__.py +0 -0
  137. digest_generator-0.1.0/tests/shared/tts/test_engine.py +53 -0
  138. digest_generator-0.1.0/tests/shared/tts/test_registry.py +115 -0
  139. digest_generator-0.1.0/tests/shared/tts/test_types.py +41 -0
  140. digest_generator-0.1.0/tests/sources/__init__.py +0 -0
  141. digest_generator-0.1.0/tests/sources/rss/__init__.py +0 -0
  142. digest_generator-0.1.0/tests/sources/rss/test_config.py +254 -0
  143. digest_generator-0.1.0/tests/sources/rss/test_fetcher.py +474 -0
  144. digest_generator-0.1.0/tests/sources/rss/test_io.py +185 -0
  145. digest_generator-0.1.0/tests/sources/rss/test_types.py +65 -0
  146. digest_generator-0.1.0/tests/test_api.py +648 -0
  147. digest_generator-0.1.0/tests/test_cli_audio.py +144 -0
  148. digest_generator-0.1.0/uv.lock +1559 -0
@@ -0,0 +1,131 @@
1
+ # =============================================================================
2
+ # Digest Generator Environment Configuration
3
+ # =============================================================================
4
+ # Copy this file to .env and fill in the required values.
5
+ # Only HF_TOKEN is required. Everything else has a sensible default.
6
+ # See digest_generator/shared/settings.py for the full list of values.
7
+ # Sampling knobs and CLI flags are documented in docs/usage.md.
8
+ # =============================================================================
9
+
10
+ # -- Required --
11
+ HF_TOKEN= # HuggingFace API token (used by the BART-MNLI classifier)
12
+
13
+ # =============================================================================
14
+ # Foundational / shared infrastructure
15
+ # =============================================================================
16
+
17
+ # -- Device --
18
+ # DEVICE=cpu # cpu, cuda, or mps (used by the classifier)
19
+
20
+ # -- Ollama (shared by the summarizer and every digest stage) --
21
+ # OLLAMA_HOST=http://localhost:11434 # Local Ollama URL
22
+ # OLLAMA_API_KEY= # Set to use cloud Ollama (auto-detected)
23
+ # OLLAMA_CONCURRENCY=3 # Global cap on in-flight Ollama calls
24
+ # OLLAMA_READ_TIMEOUT_S=300 # Read timeout per call (seconds)
25
+
26
+ # =============================================================================
27
+ # Pipeline stages (in execution order)
28
+ # =============================================================================
29
+
30
+ # -- Feeds source (RSS feed discovery) --
31
+ # Feeds are defined in a feeds.yaml (see feeds.example.yaml). When neither is
32
+ # set, the loader searches ./digest-generator/feeds.yaml then
33
+ # ~/.config/digest-generator/feeds.yaml.
34
+ # FEEDS_FILE= # Explicit path to a feeds.yaml file
35
+ # DIGEST_CONFIG= # Config directory holding feeds.yaml (+ prompts/)
36
+ # PROMPTS_DIR= # Directory of prompt-template overrides (<name>.md)
37
+
38
+ # -- Fetcher --
39
+ # FETCH_TIMEOUT=10 # HTTP request timeout (seconds)
40
+ # FETCH_RATE_LIMIT=0.4 # Delay between article fetches (seconds)
41
+ # FETCH_CONCURRENCY=10 # Max concurrent feed fetches
42
+ # MIN_CONTENT_LENGTH=200 # Min chars for the article quality check
43
+ # MAX_BOILERPLATE_RATIO=0.05 # Max boilerplate fraction before rejection
44
+
45
+ # -- Summarizer (per-article fact extraction) --
46
+ # Per-article LLM call producing a fact-dense 2-4 sentence summary.
47
+ # Concurrency is capped by SUMMARIZER_CONCURRENCY.
48
+ # SUMMARIZER_MODEL=gemma4:31b-cloud
49
+ # SUMMARIZER_TEMPERATURE=0.2
50
+ # SUMMARIZER_TOP_P= # unset uses the Ollama default
51
+ # SUMMARIZER_REPETITION_PENALTY=
52
+ # SUMMARIZER_SEED=
53
+ # SUMMARIZER_CONCURRENCY=8 # Max concurrent per-article LLM calls
54
+ # CONTENT_HEAD_MAX_CHARS=2000 # Max chars of raw article text passed to the digest writer
55
+
56
+ # -- Topic classifier (BART-MNLI zero-shot) --
57
+ # TOPIC_MODEL=facebook/bart-large-mnli
58
+ # TOPIC_REVISION=d7645e127eaf1aefc7862fd59a17a5aa8558b8ce
59
+ # TOPIC_THRESHOLD=0.5 # Min probability to accept a label
60
+ # TOPIC_MAX_LENGTH=512 # Max input length in tokens
61
+
62
+ # -- Digest stage models --
63
+ # Writer drafts each section; Editorial cleans up the prose; Framer generates
64
+ # the title and intro; Watcher produces the cross-section watch list.
65
+ # Framer and Watcher fall back to WRITER_MODEL when unset.
66
+ # WRITER_MODEL=gemma4:31b-cloud
67
+ # EDITORIAL_MODEL=gpt-oss:120b-cloud
68
+ # FRAMER_MODEL= # unset, falls back to WRITER_MODEL
69
+ # WATCHER_MODEL= # unset, falls back to WRITER_MODEL
70
+ # CLUSTERER_MODEL=gpt-oss:120b-cloud # groups related articles into stories
71
+
72
+ # -- Digest stage temperatures --
73
+ # WRITER_TEMPERATURE=0.4
74
+ # EDITORIAL_TEMPERATURE=0.4
75
+ # FRAMER_TEMPERATURE=0.4
76
+ # WATCHER_TEMPERATURE=0.4
77
+ # CLUSTERER_TEMPERATURE=0.4
78
+
79
+ # -- Digest stage sampling knobs (all unset by default, so Ollama defaults apply) --
80
+ # Each stage exposes top_p (nucleus cutoff), repetition_penalty, and seed
81
+ # (for reproducible output). The `digest` subcommand exposes matching CLI
82
+ # flags (--writer-seed N, and so on) plus a global --seed N shortcut.
83
+ # WRITER_TOP_P=
84
+ # WRITER_REPETITION_PENALTY=
85
+ # WRITER_SEED=
86
+ # EDITORIAL_TOP_P=
87
+ # EDITORIAL_REPETITION_PENALTY=
88
+ # EDITORIAL_SEED=
89
+ # FRAMER_TOP_P=
90
+ # FRAMER_REPETITION_PENALTY=
91
+ # FRAMER_SEED=
92
+ # WATCHER_TOP_P=
93
+ # WATCHER_REPETITION_PENALTY=
94
+ # WATCHER_SEED=
95
+ # CLUSTERER_TOP_P=
96
+ # CLUSTERER_REPETITION_PENALTY=
97
+ # CLUSTERER_SEED=
98
+
99
+ # -- Writer extras --
100
+ # WRITER_SECTION_BATCH_SIZE=30 # Max articles per writer batch
101
+
102
+ # -- Audio (opt-in TTS narration via Piper + ffmpeg) --
103
+ # Off by default. Enable per invocation with --audio on `digest-generator run`
104
+ # or `digest-generator digest`, or with the standalone `digest-generator audio`.
105
+ # AUDIO_ENABLED=false # Enable the audio renderer
106
+ # AUDIO_VOICE_MODEL=en_US-amy-medium # Piper voice id from rhasspy/piper-voices
107
+ # AUDIO_VOICE_REVISION=375a0fe641dea077c2a47b4e9a056d6da521eed3
108
+ # Pinned voice-repo revision
109
+ # AUDIO_BITRATE_KBPS=24 # Opus encode bitrate (mono spoken word)
110
+ # AUDIO_SAMPLE_RATE=22050 # Piper sample rate (voice-dependent)
111
+ # AUDIO_VOICE_CACHE=~/.cache/digest_generator/piper-voices
112
+ # Cache for downloaded Piper voice files
113
+ # AUDIO_FFMPEG_PATH=ffmpeg # Override the ffmpeg binary path
114
+ # AUDIO_SENTENCE_SILENCE_S=0.4 # Pause length between sentences (seconds)
115
+
116
+ # =============================================================================
117
+ # Pipeline-level
118
+ # =============================================================================
119
+
120
+ # DAYS_BACK=7 # How many days back to fetch articles
121
+ # OUTPUT_DIR=output # Root output directory
122
+
123
+ # =============================================================================
124
+ # Logging
125
+ # =============================================================================
126
+
127
+ # LOG_LEVEL_CONSOLE=INFO # Console log level
128
+ # LOG_LEVEL_FILE=DEBUG # File log level (logs/digest_generator.log + run.log)
129
+ # LOG_DIR=logs # Global log file directory
130
+ # LOG_ROTATION=20 MB # Size-based rotation for the global log
131
+ # LOG_RETENTION=5 # Number of rotated files to keep
@@ -0,0 +1 @@
1
+ * @laplacef
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: Bug report
3
+ about: Report something that isn't working as expected
4
+ title: ''
5
+ labels: bug
6
+ assignees: ''
7
+ ---
8
+
9
+ ## Description
10
+
11
+ <!-- A clear and concise description of the bug. -->
12
+
13
+ ## Steps to reproduce
14
+
15
+ 1.
16
+ 2.
17
+ 3.
18
+
19
+ ## Expected behavior
20
+
21
+ <!-- What you expected to happen. -->
22
+
23
+ ## Actual behavior
24
+
25
+ <!-- What actually happened. Include error messages or logs if available. -->
26
+
27
+ ## Minimal reproduction
28
+
29
+ <!-- The smallest command, input, or code snippet that triggers the bug. -->
30
+
31
+ ## Environment
32
+
33
+ - OS:
34
+ - Python version:
35
+ - Project version / commit:
36
+
37
+ ## Additional context
38
+
39
+ <!-- Anything else that might help diagnose the problem. -->
@@ -0,0 +1,6 @@
1
+ # Require contributors to use a template instead of opening a blank issue.
2
+ blank_issues_enabled: false
3
+ contact_links:
4
+ - name: Security vulnerability
5
+ url: https://github.com/laplacef/digest-generator/security/advisories/new
6
+ about: Report security issues privately. Do not open a public issue.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea or improvement
4
+ title: ''
5
+ labels: enhancement
6
+ assignees: ''
7
+ ---
8
+
9
+ ## Problem
10
+
11
+ <!-- What problem does this solve? What are you trying to do that you can't today? -->
12
+
13
+ ## Proposed solution
14
+
15
+ <!-- The change you'd like to see. -->
16
+
17
+ ## Alternatives considered
18
+
19
+ <!-- Other approaches you thought about, and why you prefer the one above. -->
20
+
21
+ ## Additional context
22
+
23
+ <!-- Mockups, links, prior art, or anything else that clarifies the request. -->
@@ -0,0 +1,24 @@
1
+ <!-- Thanks for contributing! Complete the sections below so reviewers have what they need. -->
2
+
3
+ ## Summary
4
+
5
+ <!-- What does this PR do, and why? Link the issue it addresses. -->
6
+
7
+ Closes #<issue-number>
8
+
9
+ ## Type of change
10
+
11
+ - [ ] Feature
12
+ - [ ] Bug fix
13
+ - [ ] Documentation
14
+ - [ ] Refactor
15
+ - [ ] Other
16
+
17
+ ## Checklist
18
+
19
+ - [ ] My change follows the project's coding standards (see [CONTRIBUTING.md](../CONTRIBUTING.md)).
20
+ - [ ] Commits follow Conventional Commits.
21
+ - [ ] I ran `uv run pre-commit run --all-files` and `uv run pytest` locally.
22
+ - [ ] I added or updated tests for my change.
23
+ - [ ] I updated documentation where relevant.
24
+ - [ ] No secrets or credentials are committed (`.env` stays local).
@@ -0,0 +1,38 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ pre-commit:
15
+ name: Pre-commit (lint, format, types, security, secrets)
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v6
20
+ with:
21
+ enable-cache: true
22
+ - run: uv sync --frozen --extra dev
23
+ - uses: actions/cache@v4
24
+ with:
25
+ path: ~/.cache/pre-commit
26
+ key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
27
+ - run: uv run --no-sync pre-commit run --all-files --show-diff-on-failure
28
+
29
+ test:
30
+ name: Test
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - uses: astral-sh/setup-uv@v6
35
+ with:
36
+ enable-cache: true
37
+ - run: uv sync --frozen --extra dev
38
+ - run: uv run --no-sync pytest tests/ -v --cov=digest_generator --cov-report=term-missing
@@ -0,0 +1,103 @@
1
+ name: Release
2
+
3
+ # Tagging vX.Y.Z builds the distributions, publishes them to PyPI via OIDC
4
+ # trusted publishing, and cuts a GitHub Release with the changelog entry.
5
+ #
6
+ # Trusted publishing setup (one-time, no API token needed):
7
+ # 1. On PyPI, add a trusted publisher for project "digest-generator":
8
+ # owner=laplacef, repo=digest-generator, workflow=release.yml,
9
+ # environment=pypi.
10
+ # 2. On GitHub, create an environment named "pypi" (optionally with
11
+ # required reviewers) so the publish step is gated.
12
+
13
+ on:
14
+ push:
15
+ tags: ["v*"]
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ build:
22
+ name: Build distributions
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - uses: astral-sh/setup-uv@v6
28
+ with:
29
+ enable-cache: true
30
+
31
+ - name: Verify tag matches package version
32
+ run: |
33
+ tag="${GITHUB_REF_NAME#v}"
34
+ version=$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/')
35
+ if [ "$tag" != "$version" ]; then
36
+ echo "Tag $GITHUB_REF_NAME ($tag) does not match pyproject version $version" >&2
37
+ exit 1
38
+ fi
39
+
40
+ - name: Build sdist and wheel
41
+ run: uv build
42
+
43
+ - name: Validate distributions
44
+ run: uvx twine check dist/*
45
+
46
+ - uses: actions/upload-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+
51
+ pypi-publish:
52
+ name: Publish to PyPI
53
+ needs: build
54
+ runs-on: ubuntu-latest
55
+ environment: pypi
56
+ permissions:
57
+ id-token: write # OIDC token for trusted publishing
58
+ steps:
59
+ - uses: actions/download-artifact@v4
60
+ with:
61
+ name: dist
62
+ path: dist/
63
+
64
+ - uses: pypa/gh-action-pypi-publish@release/v1
65
+
66
+ github-release:
67
+ name: Create GitHub Release
68
+ needs: build
69
+ runs-on: ubuntu-latest
70
+ permissions:
71
+ contents: write
72
+ # Pass the tag through the environment (never interpolate ${{ }} into a
73
+ # run: script body — a tag may legally contain shell metacharacters).
74
+ env:
75
+ TAG: ${{ github.ref_name }}
76
+ steps:
77
+ - uses: actions/checkout@v4
78
+
79
+ - uses: actions/download-artifact@v4
80
+ with:
81
+ name: dist
82
+ path: dist/
83
+
84
+ - name: Extract changelog entry
85
+ run: |
86
+ version="${TAG#v}"
87
+ awk -v ver="$version" '
88
+ /^## \[/ {
89
+ if (found) exit
90
+ if (index($0, "[" ver "]")) found=1
91
+ next
92
+ }
93
+ found { print }
94
+ ' CHANGELOG.md > release_notes.md
95
+
96
+ - name: Create GitHub Release
97
+ env:
98
+ GH_TOKEN: ${{ github.token }}
99
+ run: |
100
+ gh release create "$TAG" \
101
+ --title "$TAG" \
102
+ --notes-file release_notes.md \
103
+ dist/*
@@ -0,0 +1,63 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+ env/
28
+
29
+ # Environment Variables
30
+ .env
31
+ .env.*
32
+ !.env.example
33
+
34
+ # IDE
35
+ .vscode/
36
+ .idea/
37
+ *.swp
38
+ *.swo
39
+ *~
40
+
41
+ # Ruff
42
+ .ruff_cache/
43
+
44
+ # MyPy
45
+ .mypy_cache/
46
+ .dmypy.json
47
+ dmypy.json
48
+
49
+ # Pytest / Coverage
50
+ .pytest_cache/
51
+ .coverage
52
+ htmlcov/
53
+
54
+ # Run output
55
+ output/
56
+
57
+ # Logs
58
+ logs/
59
+ *.log
60
+
61
+ # OS
62
+ .DS_Store
63
+ Thumbs.db
@@ -0,0 +1,23 @@
1
+ # gitleaks configuration.
2
+ #
3
+ # `useDefault` keeps every built-in rule active; the allowlist below only
4
+ # suppresses known false positives so the scanner stays strict everywhere else.
5
+ [extend]
6
+ useDefault = true
7
+
8
+ [allowlist]
9
+ description = "Repo-specific false-positive allowlist"
10
+
11
+ # detect-secrets stores accepted findings as SHA1 hashes; gitleaks would
12
+ # otherwise flag those hashes as generic high-entropy secrets.
13
+ paths = [
14
+ '''\.secrets\.baseline''',
15
+ ]
16
+
17
+ # Pinned, public model/voice revision identifiers (40-char git SHAs), not
18
+ # secrets: the HuggingFace topic-model revision and the Piper voice-repo
19
+ # revision. Update these if the pins in settings.py / .env.example change.
20
+ regexes = [
21
+ '''d7645e127eaf1aefc7862fd59a17a5aa8558b8ce''',
22
+ '''375a0fe641dea077c2a47b4e9a056d6da521eed3''',
23
+ ]
@@ -0,0 +1,81 @@
1
+ default_language_version:
2
+ python: python3.13
3
+
4
+ default_stages: [pre-commit]
5
+
6
+ fail_fast: false
7
+
8
+ repos:
9
+ # Basic file checks
10
+ - repo: https://github.com/pre-commit/pre-commit-hooks
11
+ rev: v6.0.0
12
+ hooks:
13
+ - id: trailing-whitespace
14
+ name: Trim trailing whitespace
15
+ - id: end-of-file-fixer
16
+ name: Fix end of files
17
+ - id: check-yaml
18
+ name: Check YAML syntax
19
+ - id: check-toml
20
+ name: Check TOML syntax
21
+ - id: check-added-large-files
22
+ name: Check for large files
23
+ args: ['--maxkb=1000']
24
+ - id: check-case-conflict
25
+ name: Check for case conflicts
26
+ - id: check-merge-conflict
27
+ name: Check for merge conflicts
28
+ - id: detect-private-key
29
+ name: Detect private keys
30
+
31
+ # Project tools run via `uv run` as local hooks so pre-commit and CI use the
32
+ # same tool version (the uv-locked one) and the same project-aware config in
33
+ # pyproject.toml. No drift between hook `rev` pins and the lockfile, and no
34
+ # weaker isolated mypy environment.
35
+ #
36
+ # `--no-sync` runs each tool from the already-synced dev venv without
37
+ # re-syncing. A bare `uv run <tool>` would drop the `dev` extra mid-run.
38
+ # Requires `uv sync --extra dev` first (see CONTRIBUTING.md and CI).
39
+ - repo: local
40
+ hooks:
41
+ - id: ruff
42
+ name: Ruff linter
43
+ entry: uv run --no-sync ruff check --fix
44
+ language: system
45
+ types_or: [python, pyi]
46
+ require_serial: true
47
+ - id: ruff-format
48
+ name: Ruff formatter
49
+ entry: uv run --no-sync ruff format
50
+ language: system
51
+ types_or: [python, pyi]
52
+ require_serial: true
53
+ - id: mypy
54
+ name: Type checking with mypy
55
+ entry: uv run --no-sync mypy digest_generator/
56
+ language: system
57
+ types: [python]
58
+ pass_filenames: false
59
+ - id: bandit
60
+ name: Security checks with Bandit
61
+ entry: uv run --no-sync bandit -c pyproject.toml -r digest_generator/
62
+ language: system
63
+ types: [python]
64
+ pass_filenames: false
65
+
66
+ # Secret detection with Yelp/detect-secrets (entropy and regex scanner).
67
+ - repo: https://github.com/Yelp/detect-secrets
68
+ rev: v1.5.0
69
+ hooks:
70
+ - id: detect-secrets
71
+ name: Secret detection (detect-secrets)
72
+ args: ["--baseline", ".secrets.baseline"]
73
+ exclude: ^(\.secrets\.baseline|\.gitleaks\.toml|uv\.lock)
74
+
75
+ # Secret detection with gitleaks, a regex scanner with broader default
76
+ # rule coverage than detect-secrets.
77
+ - repo: https://github.com/gitleaks/gitleaks
78
+ rev: v8.30.1
79
+ hooks:
80
+ - id: gitleaks
81
+ name: Secret detection (gitleaks)
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,133 @@
1
+ {
2
+ "version": "1.5.0",
3
+ "plugins_used": [
4
+ {
5
+ "name": "ArtifactoryDetector"
6
+ },
7
+ {
8
+ "name": "AWSKeyDetector"
9
+ },
10
+ {
11
+ "name": "AzureStorageKeyDetector"
12
+ },
13
+ {
14
+ "name": "Base64HighEntropyString",
15
+ "limit": 4.5
16
+ },
17
+ {
18
+ "name": "BasicAuthDetector"
19
+ },
20
+ {
21
+ "name": "CloudantDetector"
22
+ },
23
+ {
24
+ "name": "DiscordBotTokenDetector"
25
+ },
26
+ {
27
+ "name": "GitHubTokenDetector"
28
+ },
29
+ {
30
+ "name": "GitLabTokenDetector"
31
+ },
32
+ {
33
+ "name": "HexHighEntropyString",
34
+ "limit": 3.0
35
+ },
36
+ {
37
+ "name": "IbmCloudIamDetector"
38
+ },
39
+ {
40
+ "name": "IbmCosHmacDetector"
41
+ },
42
+ {
43
+ "name": "IPPublicDetector"
44
+ },
45
+ {
46
+ "name": "JwtTokenDetector"
47
+ },
48
+ {
49
+ "name": "KeywordDetector",
50
+ "keyword_exclude": ""
51
+ },
52
+ {
53
+ "name": "MailchimpDetector"
54
+ },
55
+ {
56
+ "name": "NpmDetector"
57
+ },
58
+ {
59
+ "name": "OpenAIDetector"
60
+ },
61
+ {
62
+ "name": "PrivateKeyDetector"
63
+ },
64
+ {
65
+ "name": "PypiTokenDetector"
66
+ },
67
+ {
68
+ "name": "SendGridDetector"
69
+ },
70
+ {
71
+ "name": "SlackDetector"
72
+ },
73
+ {
74
+ "name": "SoftlayerDetector"
75
+ },
76
+ {
77
+ "name": "SquareOAuthDetector"
78
+ },
79
+ {
80
+ "name": "StripeDetector"
81
+ },
82
+ {
83
+ "name": "TelegramBotTokenDetector"
84
+ },
85
+ {
86
+ "name": "TwilioKeyDetector"
87
+ }
88
+ ],
89
+ "filters_used": [
90
+ {
91
+ "path": "detect_secrets.filters.allowlist.is_line_allowlisted"
92
+ },
93
+ {
94
+ "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
95
+ "min_level": 2
96
+ },
97
+ {
98
+ "path": "detect_secrets.filters.heuristic.is_indirect_reference"
99
+ },
100
+ {
101
+ "path": "detect_secrets.filters.heuristic.is_likely_id_string"
102
+ },
103
+ {
104
+ "path": "detect_secrets.filters.heuristic.is_lock_file"
105
+ },
106
+ {
107
+ "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
108
+ },
109
+ {
110
+ "path": "detect_secrets.filters.heuristic.is_potential_uuid"
111
+ },
112
+ {
113
+ "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
114
+ },
115
+ {
116
+ "path": "detect_secrets.filters.heuristic.is_sequential_string"
117
+ },
118
+ {
119
+ "path": "detect_secrets.filters.heuristic.is_swagger_file"
120
+ },
121
+ {
122
+ "path": "detect_secrets.filters.heuristic.is_templated_secret"
123
+ },
124
+ {
125
+ "path": "detect_secrets.filters.regex.should_exclude_file",
126
+ "pattern": [
127
+ "\\.venv|\\.mypy_cache|samples|uv\\.lock|node_modules"
128
+ ]
129
+ }
130
+ ],
131
+ "results": {},
132
+ "generated_at": "2026-05-13T21:55:02Z"
133
+ }