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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: 2
updates:
# Keep GitHub Actions pinned SHAs up to date
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"

# Keep Python dependencies updated (creates PRs for lockfile updates)
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore"
13 changes: 4 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,8 @@ jobs:
python-version: ["3.11", "3.12", "3.13"]

steps:
# TODO H11: pin to full 40-char commit SHA once the SHA for actions/checkout@v4 is verified
- uses: actions/checkout@v4

# TODO H11: pin to full 40-char commit SHA once the SHA for astral-sh/setup-uv@v5 is verified
- uses: astral-sh/setup-uv@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
python-version: ${{ matrix.python-version }}

Expand All @@ -39,15 +36,13 @@ jobs:
- name: Test
run: uv run pytest --cov=hyperping --cov-report=xml

# L5: dependency vulnerability scan on every CI run
- name: Audit dependencies
continue-on-error: true
run: uv run pip-audit 2>/dev/null || uv audit
run: uv run pip-audit

- name: Upload coverage to Codecov
if: matrix.python-version == '3.12'
# TODO H11: pin to full 40-char commit SHA
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
with:
files: coverage.xml
fail_ci_if_error: false
36 changes: 18 additions & 18 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,25 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
attestations: write
id-token: write
steps:
# TODO H11: pin to full 40-char commit SHA
- uses: actions/checkout@v4
# TODO H11: pin to full 40-char commit SHA
- uses: astral-sh/setup-uv@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with: { python-version: "3.12" }
- run: uv sync --all-extras
# L5: audit before publishing
- name: Audit dependencies
continue-on-error: true
run: uv run pip-audit 2>/dev/null || uv audit
- run: uv run pytest # gate — no publish on red tests
run: uv run pip-audit
- run: uv run pytest
- run: uv build
# TODO H11: pin to full 40-char commit SHA
- uses: actions/upload-artifact@v4

# SLSA provenance attestation for supply chain integrity
- name: Attest build provenance
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
with:
subject-path: dist/*

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: { name: dist, path: dist/ }

publish-testpypi:
Expand All @@ -36,11 +40,9 @@ jobs:
permissions:
id-token: write
steps:
# TODO H11: pin to full 40-char commit SHA
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: { name: dist, path: dist/ }
# TODO H11: pin to full 40-char commit SHA
- uses: pypa/gh-action-pypi-publish@release/v1
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
with:
repository-url: https://test.pypi.org/legacy/

Expand All @@ -54,8 +56,6 @@ jobs:
permissions:
id-token: write
steps:
# TODO H11: pin to full 40-char commit SHA
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: { name: dist, path: dist/ }
# TODO H11: pin to full 40-char commit SHA
- uses: pypa/gh-action-pypi-publish@release/v1
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ htmlcov/
.vscode/
.idea/

# Secrets / environment
.env
.env.*
.env.local

# OS
.DS_Store

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dev = [
"ruff>=0.4",
"mypy>=1.10",
"pydantic",
"pip-audit>=2.7",
]

[project.urls]
Expand Down
4 changes: 3 additions & 1 deletion src/hyperping/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ def parse_list(
results.append(model_cls.model_validate(item)) # type: ignore[attr-defined]
except (ValueError, ValidationError) as exc:
skipped += 1
logger.warning("Failed to parse %s data: %s", label, exc, extra={"data": item})
# Log only the exception, not the raw item - it may contain
# sensitive data (subscriber emails, custom auth headers).
logger.warning("Failed to parse %s data: %s", label, exc)

if skipped:
logger.warning(
Expand Down
12 changes: 10 additions & 2 deletions src/hyperping/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,10 @@ def __init__(
user_agent: Custom ``User-Agent`` header value. Defaults to
``hyperping-python/0.1.0``.
"""
self._api_key = SecretStr(api_key) if isinstance(api_key, str) else api_key
raw_key = api_key.get_secret_value() if isinstance(api_key, SecretStr) else api_key
if not raw_key or not raw_key.strip():
raise ValueError("api_key must be a non-empty string")
self._api_key = SecretStr(raw_key) if isinstance(api_key, str) else api_key
self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
self.timeout = timeout
self.retry_config = retry_config or DEFAULT_RETRY_CONFIG
Expand Down Expand Up @@ -268,7 +271,12 @@ def _compute_sleep_time(
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
if retry_after:
return min(float(retry_after), _RETRY_AFTER_MAX)
try:
return min(float(retry_after), _RETRY_AFTER_MAX)
except (ValueError, OverflowError):
# RFC 7231 allows HTTP-date strings in Retry-After;
# fall through to exponential backoff if unparseable.
pass
return delay + random.uniform(0, delay * 0.25)

def _should_retry(self, status_code: int, attempt: int) -> bool:
Expand Down
7 changes: 6 additions & 1 deletion src/hyperping/models/_monitor_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,12 @@ def coerce_escalation_policy(cls, v: object) -> str | None:

# Helper methods for backward compatibility
def get_headers_dict(self) -> dict[str, str]:
"""Get headers as a dictionary for convenience."""
"""Get headers as a dictionary for convenience.

Warning: The returned dict may contain sensitive values (e.g.,
Authorization tokens) that the user configured on this monitor.
Avoid logging the result without redacting sensitive keys first.
"""
return {h.name: h.value for h in self.request_headers}

@staticmethod
Expand Down
Loading
Loading