Spaces:
Running
Running
Upload 8 files
Browse files- _dataset_schema.py +7 -0
- _shared_logic.py +215 -2
- app.py +17 -12
- deduplicate_dataset.py +33 -6
_dataset_schema.py
CHANGED
|
@@ -128,6 +128,13 @@ Schema version history
|
|
| 128 |
all v2-only fields default via ``setdefault`` (``feedbackId=None``,
|
| 129 |
``prevFeedbackId=None``, ``editCount=0``).
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
References
|
| 132 |
----------
|
| 133 |
See ``DATASET_COLLECTION_GUIDANCE.md`` for the deduplication contract, the
|
|
|
|
| 128 |
all v2-only fields default via ``setdefault`` (``feedbackId=None``,
|
| 129 |
``prevFeedbackId=None``, ``editCount=0``).
|
| 130 |
|
| 131 |
+
Security note
|
| 132 |
+
Client IP addresses are **never stored** in dataset records. The proxy
|
| 133 |
+
(``app.py``) passes every ``client_ip`` through ``_mask_ip()`` before any
|
| 134 |
+
log entry is written, and the normalisation functions in this module
|
| 135 |
+
receive only the sanitised JSON payload — the raw IP never enters any
|
| 136 |
+
field accepted or emitted by this module.
|
| 137 |
+
|
| 138 |
References
|
| 139 |
----------
|
| 140 |
See ``DATASET_COLLECTION_GUIDANCE.md`` for the deduplication contract, the
|
_shared_logic.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# _shared_logic.py v6.
|
| 2 |
#
|
| 3 |
# Single source of truth for shared constants, pure helper functions, and
|
| 4 |
# type aliases used by the deployed proxy (_hf_spaces_proxy/app.py) and the
|
|
@@ -152,9 +152,11 @@ deployed Spaces and log aggregators can correlate errors to a specific release.
|
|
| 152 |
|
| 153 |
from __future__ import annotations
|
| 154 |
|
|
|
|
| 155 |
import json
|
| 156 |
import logging
|
| 157 |
import os
|
|
|
|
| 158 |
from typing import Any, Literal
|
| 159 |
|
| 160 |
logger = logging.getLogger(__name__)
|
|
@@ -186,6 +188,10 @@ __all__ = [ # noqa: RUF022
|
|
| 186 |
"_safe_float",
|
| 187 |
"_safe_int",
|
| 188 |
"_token_log_fragment",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
# Helpers — token type system (v6.1.0)
|
| 190 |
"_classify_token_type",
|
| 191 |
"_token_suitable_for_inference",
|
|
@@ -203,7 +209,7 @@ __all__ = [ # noqa: RUF022
|
|
| 203 |
# ─────────────────────────────────────────────────────────────────────────────
|
| 204 |
|
| 205 |
#: Proxy release version — bump on every breaking change.
|
| 206 |
-
PROXY_VERSION: str = "6.
|
| 207 |
|
| 208 |
#: HuggingFace Inference Providers router base URL (no trailing slash).
|
| 209 |
#: Only used for Path 3 (standard provider models) when ``BACKEND_URL`` is
|
|
@@ -611,6 +617,213 @@ def _token_log_fragment(token: str, token_type: str = "") -> str:
|
|
| 611 |
return fragment
|
| 612 |
|
| 613 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
def _classify_token_type(
|
| 615 |
token: str,
|
| 616 |
declared_type: str | None = None,
|
|
|
|
| 1 |
+
# _shared_logic.py v6.2.0
|
| 2 |
#
|
| 3 |
# Single source of truth for shared constants, pure helper functions, and
|
| 4 |
# type aliases used by the deployed proxy (_hf_spaces_proxy/app.py) and the
|
|
|
|
| 152 |
|
| 153 |
from __future__ import annotations
|
| 154 |
|
| 155 |
+
import ipaddress
|
| 156 |
import json
|
| 157 |
import logging
|
| 158 |
import os
|
| 159 |
+
import re
|
| 160 |
from typing import Any, Literal
|
| 161 |
|
| 162 |
logger = logging.getLogger(__name__)
|
|
|
|
| 188 |
"_safe_float",
|
| 189 |
"_safe_int",
|
| 190 |
"_token_log_fragment",
|
| 191 |
+
# Privacy / log-redaction (v6.2.0)
|
| 192 |
+
"_REDACT_PATTERNS",
|
| 193 |
+
"_RedactingFilter",
|
| 194 |
+
"_mask_ip",
|
| 195 |
# Helpers — token type system (v6.1.0)
|
| 196 |
"_classify_token_type",
|
| 197 |
"_token_suitable_for_inference",
|
|
|
|
| 209 |
# ─────────────────────────────────────────────────────────────────────────────
|
| 210 |
|
| 211 |
#: Proxy release version — bump on every breaking change.
|
| 212 |
+
PROXY_VERSION: str = "6.2.0"
|
| 213 |
|
| 214 |
#: HuggingFace Inference Providers router base URL (no trailing slash).
|
| 215 |
#: Only used for Path 3 (standard provider models) when ``BACKEND_URL`` is
|
|
|
|
| 617 |
return fragment
|
| 618 |
|
| 619 |
|
| 620 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 621 |
+
# Privacy / log-redaction helpers (v6.2.0)
|
| 622 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 623 |
+
#
|
| 624 |
+
# Design rationale
|
| 625 |
+
# ----------------
|
| 626 |
+
# Two complementary layers protect PII in log output:
|
| 627 |
+
#
|
| 628 |
+
# Layer 1 — call-site masking via :func:`_mask_ip`
|
| 629 |
+
# Every ``json.dumps({..., "ip": ...})`` call in ``app.py`` passes
|
| 630 |
+
# ``client_ip`` through :func:`_mask_ip` before it is serialised.
|
| 631 |
+
# This is the PRIMARY control: the raw IP never enters the log string.
|
| 632 |
+
#
|
| 633 |
+
# Layer 2 — defence-in-depth via :class:`_RedactingFilter`
|
| 634 |
+
# Attached to the root logging handler. Applies :data:`_REDACT_PATTERNS`
|
| 635 |
+
# to the fully formatted message BEFORE it is emitted. Catches:
|
| 636 |
+
# • HF token strings leaked via exception messages from
|
| 637 |
+
# ``huggingface_hub`` (e.g. ``snapshot_download`` auth failures).
|
| 638 |
+
# • IPv4 addresses emitted by third-party library loggers (httpx,
|
| 639 |
+
# uvicorn) that bypass the call-site masking.
|
| 640 |
+
# • Any future code that forgets to call :func:`_mask_ip` first.
|
| 641 |
+
#
|
| 642 |
+
# IPv6 is handled exclusively at Layer 1 (:func:`_mask_ip`). A generic
|
| 643 |
+
# IPv6 regex in Layer 2 has unacceptable false-positive rates (e.g. it
|
| 644 |
+
# would match ``12:34:56:78`` in log timestamps or MAC addresses).
|
| 645 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def _mask_ip(ip: str) -> str:
|
| 649 |
+
"""Mask a client IP address for privacy-safe log output.
|
| 650 |
+
|
| 651 |
+
Preserves enough network context for rate-limit and abuse analysis while
|
| 652 |
+
zeroing the host portion that identifies the individual user.
|
| 653 |
+
|
| 654 |
+
* **IPv4** — zero the last octet, retaining the /24 subnet.
|
| 655 |
+
``"192.168.1.100"`` → ``"192.168.1.0"``
|
| 656 |
+
* **IPv6** — zero the interface identifier (last 64 bits), retaining
|
| 657 |
+
the /64 prefix. ``"2001:db8:85a3::8a2e:370:7334"`` → ``"2001:db8:85a3::"``
|
| 658 |
+
* **IPv6 scope suffix** (e.g. ``"fe80::1%eth0"``) — stripped before
|
| 659 |
+
parsing (Python's :mod:`ipaddress` does not accept scope identifiers).
|
| 660 |
+
* **Non-IP strings** — returned as ``"<ip-redacted>"``.
|
| 661 |
+
* **Sentinel** ``"unknown"`` — returned unchanged (already non-identifying).
|
| 662 |
+
|
| 663 |
+
Parameters
|
| 664 |
+
----------
|
| 665 |
+
ip : str
|
| 666 |
+
Client IP string extracted from the HTTP request headers.
|
| 667 |
+
May be ``"unknown"`` when the proxy header is absent.
|
| 668 |
+
|
| 669 |
+
Returns
|
| 670 |
+
-------
|
| 671 |
+
str
|
| 672 |
+
Masked IP suitable for structured log output. This function is
|
| 673 |
+
intentionally never-raise — any :exc:`ValueError` from
|
| 674 |
+
:mod:`ipaddress` is caught and replaced by the safe fallback.
|
| 675 |
+
|
| 676 |
+
Notes
|
| 677 |
+
-----
|
| 678 |
+
**Security note** — This is the canonical privacy gate for all IP values
|
| 679 |
+
written to log records in ``app.py``. Every ``json.dumps({..., "ip": …})``
|
| 680 |
+
call must pass ``client_ip`` through :func:`_mask_ip` before serialising.
|
| 681 |
+
Callers must **not** write raw ``client_ip`` values to any log record.
|
| 682 |
+
|
| 683 |
+
**Developer note** — Uses :mod:`ipaddress` from the Python standard
|
| 684 |
+
library; no third-party dependencies are introduced.
|
| 685 |
+
|
| 686 |
+
Examples
|
| 687 |
+
--------
|
| 688 |
+
>>> _mask_ip("192.168.1.100")
|
| 689 |
+
'192.168.1.0'
|
| 690 |
+
>>> _mask_ip("10.0.0.255")
|
| 691 |
+
'10.0.0.0'
|
| 692 |
+
>>> _mask_ip("2001:db8:85a3::8a2e:370:7334")
|
| 693 |
+
'2001:db8:85a3::'
|
| 694 |
+
>>> _mask_ip("fe80::1%eth0")
|
| 695 |
+
'fe80::'
|
| 696 |
+
>>> _mask_ip("unknown")
|
| 697 |
+
'unknown'
|
| 698 |
+
>>> _mask_ip("not-an-ip")
|
| 699 |
+
'<ip-redacted>'
|
| 700 |
+
"""
|
| 701 |
+
if ip in ("unknown", ""):
|
| 702 |
+
return ip
|
| 703 |
+
try:
|
| 704 |
+
# Strip IPv6 zone/scope identifier (e.g. "%eth0") — ipaddress rejects it.
|
| 705 |
+
clean: str = ip.split("%", 1)[0].strip()
|
| 706 |
+
addr = ipaddress.ip_address(clean)
|
| 707 |
+
if isinstance(addr, ipaddress.IPv4Address):
|
| 708 |
+
# Retain /24 (first three octets); zero the host octet.
|
| 709 |
+
return str(
|
| 710 |
+
ipaddress.ip_network(f"{addr}/24", strict=False).network_address
|
| 711 |
+
)
|
| 712 |
+
# IPv6: retain /64 prefix; zero the 64-bit interface identifier.
|
| 713 |
+
return str(
|
| 714 |
+
ipaddress.ip_network(f"{addr}/64", strict=False).network_address
|
| 715 |
+
)
|
| 716 |
+
except ValueError:
|
| 717 |
+
return "<ip-redacted>"
|
| 718 |
+
|
| 719 |
+
|
| 720 |
+
#: Ordered list of ``(compiled_pattern, replacement)`` tuples applied by
|
| 721 |
+
#: :class:`_RedactingFilter` to every log record before emission.
|
| 722 |
+
#:
|
| 723 |
+
#: **Pattern order matters** — patterns are applied left-to-right; more
|
| 724 |
+
#: specific patterns must precede catch-all patterns. There is no overlap
|
| 725 |
+
#: between the current patterns, but this convention must be maintained when
|
| 726 |
+
#: extending this list.
|
| 727 |
+
#:
|
| 728 |
+
#: IPv6 addresses are intentionally **absent** — they are handled at the
|
| 729 |
+
#: call-site by :func:`_mask_ip` (Layer 1). A generic IPv6 regex in a
|
| 730 |
+
#: global filter produces too many false positives (hex timestamps, MAC
|
| 731 |
+
#: addresses, Docker overlay IDs) to be safe in a production log stream.
|
| 732 |
+
_REDACT_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
| 733 |
+
# HuggingFace API tokens — ``hf_`` prefix followed by ≥ 4 alphanumeric
|
| 734 |
+
# characters. Classic tokens are ~34 chars; fine-grained tokens are ≥ 52.
|
| 735 |
+
# The {4,} lower bound avoids matching ``hf_`` in legitimate identifiers
|
| 736 |
+
# (e.g. Python identifiers that start with ``hf_``) while still catching
|
| 737 |
+
# any partial token fragment that huggingface_hub may embed in an error
|
| 738 |
+
# message.
|
| 739 |
+
(re.compile(r"\bhf_[a-zA-Z0-9]{4,}\b"), "<token-redacted>"),
|
| 740 |
+
# IPv4 addresses — strict dotted-decimal notation with per-octet range
|
| 741 |
+
# validation (0–255). Word boundaries prevent partial matches inside
|
| 742 |
+
# longer numeric strings. This pattern catches IPv4 strings emitted by
|
| 743 |
+
# third-party loggers (httpx, uvicorn) that bypass :func:`_mask_ip`.
|
| 744 |
+
(
|
| 745 |
+
re.compile(
|
| 746 |
+
r"\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}"
|
| 747 |
+
r"(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b"
|
| 748 |
+
),
|
| 749 |
+
"<ipv4-redacted>",
|
| 750 |
+
),
|
| 751 |
+
]
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
class _RedactingFilter(logging.Filter):
|
| 755 |
+
"""Scrub sensitive values from log records before emission.
|
| 756 |
+
|
| 757 |
+
Applies the regex patterns in :data:`_REDACT_PATTERNS` to the fully
|
| 758 |
+
formatted log message, replacing HuggingFace API tokens and raw IPv4
|
| 759 |
+
addresses with opaque placeholders.
|
| 760 |
+
|
| 761 |
+
This class is the **defence-in-depth layer** (Layer 2). The primary
|
| 762 |
+
control is :func:`_mask_ip` at each call site (Layer 1). The filter
|
| 763 |
+
catches values that slip through Layer 1 — most importantly, HF token
|
| 764 |
+
strings embedded in exception messages from ``huggingface_hub``.
|
| 765 |
+
|
| 766 |
+
Parameters
|
| 767 |
+
----------
|
| 768 |
+
name : str, optional
|
| 769 |
+
Filter name forwarded to :class:`logging.Filter`. Default ``""``.
|
| 770 |
+
|
| 771 |
+
Notes
|
| 772 |
+
-----
|
| 773 |
+
**Security note** — This filter materialises the fully formatted message
|
| 774 |
+
via :meth:`logging.LogRecord.getMessage`, applies every pattern in
|
| 775 |
+
:data:`_REDACT_PATTERNS`, then replaces :attr:`~logging.LogRecord.msg`
|
| 776 |
+
with the scrubbed result and clears :attr:`~logging.LogRecord.args`.
|
| 777 |
+
Clearing ``args`` prevents downstream handlers from re-applying ``%``
|
| 778 |
+
formatting to a string that no longer contains positional placeholders.
|
| 779 |
+
|
| 780 |
+
**Developer note** — Attach to the root handler immediately after
|
| 781 |
+
construction so **every** handler in the process benefits::
|
| 782 |
+
|
| 783 |
+
handler = logging.StreamHandler()
|
| 784 |
+
handler.addFilter(_RedactingFilter())
|
| 785 |
+
logging.root.handlers = [handler]
|
| 786 |
+
|
| 787 |
+
To extend the redaction vocabulary, append a ``(pattern, replacement)``
|
| 788 |
+
tuple to :data:`_REDACT_PATTERNS`.
|
| 789 |
+
|
| 790 |
+
Examples
|
| 791 |
+
--------
|
| 792 |
+
>>> import logging
|
| 793 |
+
>>> f = _RedactingFilter()
|
| 794 |
+
>>> rec = logging.makeLogRecord({"msg": "token=hf_abc1234defg5678 ip=10.0.1.99", "args": ()})
|
| 795 |
+
>>> f.filter(rec)
|
| 796 |
+
True
|
| 797 |
+
>>> rec.msg
|
| 798 |
+
'token=<token-redacted> ip=<ipv4-redacted>'
|
| 799 |
+
"""
|
| 800 |
+
|
| 801 |
+
def filter(self, record: logging.LogRecord) -> bool: # noqa: A003
|
| 802 |
+
"""Redact sensitive patterns from *record*'s formatted message.
|
| 803 |
+
|
| 804 |
+
Parameters
|
| 805 |
+
----------
|
| 806 |
+
record : logging.LogRecord
|
| 807 |
+
Log record to inspect and mutate in-place.
|
| 808 |
+
|
| 809 |
+
Returns
|
| 810 |
+
-------
|
| 811 |
+
bool
|
| 812 |
+
Always ``True`` — this filter never suppresses records, only
|
| 813 |
+
scrubs their message content.
|
| 814 |
+
"""
|
| 815 |
+
# Materialise the full %-formatted string first, then scrub it.
|
| 816 |
+
msg: str = record.getMessage()
|
| 817 |
+
for pattern, replacement in _REDACT_PATTERNS:
|
| 818 |
+
msg = pattern.sub(replacement, msg)
|
| 819 |
+
# Write the scrubbed text back and clear args so that any subsequent
|
| 820 |
+
# call to getMessage() returns the already-scrubbed string without
|
| 821 |
+
# attempting to re-apply % formatting.
|
| 822 |
+
record.msg = msg
|
| 823 |
+
record.args = ()
|
| 824 |
+
return True
|
| 825 |
+
|
| 826 |
+
|
| 827 |
def _classify_token_type(
|
| 828 |
token: str,
|
| 829 |
declared_type: str | None = None,
|
app.py
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
# Authors: The scikit-plots developers
|
| 6 |
# SPDX-License-Identifier: BSD-3-Clause
|
| 7 |
#
|
| 8 |
-
# scikit-plots/ai · _hf_spaces_proxy/app.py v6.
|
| 9 |
#
|
| 10 |
# Thin OpenAI-compatible reverse proxy for sphinx-ai-assistant.
|
| 11 |
#
|
|
@@ -159,7 +159,9 @@ try:
|
|
| 159 |
DEFAULT_PATH3_READ_TIMEOUT,
|
| 160 |
DEFAULT_PROXY_TIMEOUT,
|
| 161 |
PROXY_VERSION,
|
|
|
|
| 162 |
_classify_token_type,
|
|
|
|
| 163 |
_resolve_upstream_url,
|
| 164 |
_safe_float,
|
| 165 |
_safe_int,
|
|
@@ -167,7 +169,7 @@ try:
|
|
| 167 |
_validate_env,
|
| 168 |
_validate_token_config,
|
| 169 |
)
|
| 170 |
-
except Exception
|
| 171 |
from _shared_logic import ( # type: ignore[import]
|
| 172 |
DEFAULT_HF_BASE,
|
| 173 |
DEFAULT_HF_SPACES_MODEL_NAMESPACES,
|
|
@@ -178,7 +180,9 @@ except Exception as e:
|
|
| 178 |
DEFAULT_PATH3_READ_TIMEOUT,
|
| 179 |
DEFAULT_PROXY_TIMEOUT,
|
| 180 |
PROXY_VERSION,
|
|
|
|
| 181 |
_classify_token_type,
|
|
|
|
| 182 |
_resolve_upstream_url,
|
| 183 |
_safe_float,
|
| 184 |
_safe_int,
|
|
@@ -257,6 +261,7 @@ class _StructuredFormatter(logging.Formatter):
|
|
| 257 |
|
| 258 |
_handler = logging.StreamHandler()
|
| 259 |
_handler.setFormatter(_StructuredFormatter())
|
|
|
|
| 260 |
logging.root.handlers = [_handler]
|
| 261 |
logging.root.setLevel(logging.INFO)
|
| 262 |
logger = logging.getLogger(__name__)
|
|
@@ -1246,7 +1251,7 @@ async def contribute(request: Request) -> JSONResponse: # noqa: PLR0912
|
|
| 1246 |
_contrib_rl[client_ip] = (count, window_start)
|
| 1247 |
if count > 5: # noqa: PLR2004
|
| 1248 |
logger.warning(
|
| 1249 |
-
json.dumps({"event": "contribute.ratelimit", "ip": client_ip})
|
| 1250 |
)
|
| 1251 |
raise HTTPException(
|
| 1252 |
status_code=429,
|
|
@@ -1355,7 +1360,7 @@ async def contribute(request: Request) -> JSONResponse: # noqa: PLR0912
|
|
| 1355 |
) from exc
|
| 1356 |
|
| 1357 |
logger.info(
|
| 1358 |
-
json.dumps({"event": "contribute.write", "rows": len(records), "ip": client_ip})
|
| 1359 |
)
|
| 1360 |
return JSONResponse({"contributed": True, "rows": len(records)})
|
| 1361 |
|
|
@@ -1436,7 +1441,7 @@ async def share(request: Request) -> JSONResponse:
|
|
| 1436 |
count += 1
|
| 1437 |
_share_rl[client_ip] = (count, window_start)
|
| 1438 |
if count > 10: # noqa: PLR2004
|
| 1439 |
-
logger.warning(json.dumps({"event": "share.ratelimit", "ip": client_ip}))
|
| 1440 |
raise HTTPException(
|
| 1441 |
status_code=429,
|
| 1442 |
detail="Rate limit exceeded. Maximum 10 shares per hour.",
|
|
@@ -1469,7 +1474,7 @@ async def share(request: Request) -> JSONResponse:
|
|
| 1469 |
{
|
| 1470 |
"event": "share.create",
|
| 1471 |
"id": share_id,
|
| 1472 |
-
"ip": client_ip,
|
| 1473 |
"ttl_days": ttl_days,
|
| 1474 |
}
|
| 1475 |
)
|
|
@@ -1613,7 +1618,7 @@ async def share_patch(share_id: str, request: Request) -> JSONResponse:
|
|
| 1613 |
count += 1
|
| 1614 |
_share_rl[client_ip] = (count, window_start)
|
| 1615 |
if count > 10: # noqa: PLR2004
|
| 1616 |
-
logger.warning(json.dumps({"event": "share.ratelimit", "ip": client_ip}))
|
| 1617 |
raise HTTPException(
|
| 1618 |
status_code=429,
|
| 1619 |
detail="Rate limit exceeded. Maximum 10 shares per hour.",
|
|
@@ -1660,7 +1665,7 @@ async def share_patch(share_id: str, request: Request) -> JSONResponse:
|
|
| 1660 |
{
|
| 1661 |
"event": "share.update",
|
| 1662 |
"id": share_id,
|
| 1663 |
-
"ip": client_ip,
|
| 1664 |
"ttl_days": ttl_days,
|
| 1665 |
}
|
| 1666 |
)
|
|
@@ -1778,7 +1783,7 @@ async def feedback(request: Request) -> JSONResponse:
|
|
| 1778 |
json.dumps(
|
| 1779 |
{
|
| 1780 |
"event": "feedback.retract",
|
| 1781 |
-
"ip": client_ip,
|
| 1782 |
"prevSessionId": payload.get("prevSessionId"),
|
| 1783 |
"conversationId": payload.get("conversationId"),
|
| 1784 |
"answerIndex": payload.get("answerIndex"),
|
|
@@ -1807,7 +1812,7 @@ async def feedback(request: Request) -> JSONResponse:
|
|
| 1807 |
_feedback_rl[client_ip] = (count, window_start)
|
| 1808 |
if count > 30: # noqa: PLR2004
|
| 1809 |
logger.warning(
|
| 1810 |
-
json.dumps({"event": "feedback.ratelimit", "ip": client_ip})
|
| 1811 |
)
|
| 1812 |
raise HTTPException(
|
| 1813 |
status_code=429,
|
|
@@ -1819,7 +1824,7 @@ async def feedback(request: Request) -> JSONResponse:
|
|
| 1819 |
json.dumps(
|
| 1820 |
{
|
| 1821 |
"event": "feedback.receive",
|
| 1822 |
-
"ip": client_ip,
|
| 1823 |
"ratingValue": payload.get("ratingValue"),
|
| 1824 |
"model": payload.get("model"),
|
| 1825 |
"conversationId": payload.get("conversationId"),
|
|
@@ -1898,7 +1903,7 @@ async def feedback(request: Request) -> JSONResponse:
|
|
| 1898 |
"event": "feedback.persist_ok",
|
| 1899 |
"retract": is_retract,
|
| 1900 |
"dedup_key": f"{conversation_id}:{answer_index}",
|
| 1901 |
-
"ip": client_ip,
|
| 1902 |
}
|
| 1903 |
)
|
| 1904 |
)
|
|
|
|
| 5 |
# Authors: The scikit-plots developers
|
| 6 |
# SPDX-License-Identifier: BSD-3-Clause
|
| 7 |
#
|
| 8 |
+
# scikit-plots/ai · _hf_spaces_proxy/app.py v6.2.0
|
| 9 |
#
|
| 10 |
# Thin OpenAI-compatible reverse proxy for sphinx-ai-assistant.
|
| 11 |
#
|
|
|
|
| 159 |
DEFAULT_PATH3_READ_TIMEOUT,
|
| 160 |
DEFAULT_PROXY_TIMEOUT,
|
| 161 |
PROXY_VERSION,
|
| 162 |
+
_RedactingFilter,
|
| 163 |
_classify_token_type,
|
| 164 |
+
_mask_ip,
|
| 165 |
_resolve_upstream_url,
|
| 166 |
_safe_float,
|
| 167 |
_safe_int,
|
|
|
|
| 169 |
_validate_env,
|
| 170 |
_validate_token_config,
|
| 171 |
)
|
| 172 |
+
except Exception:
|
| 173 |
from _shared_logic import ( # type: ignore[import]
|
| 174 |
DEFAULT_HF_BASE,
|
| 175 |
DEFAULT_HF_SPACES_MODEL_NAMESPACES,
|
|
|
|
| 180 |
DEFAULT_PATH3_READ_TIMEOUT,
|
| 181 |
DEFAULT_PROXY_TIMEOUT,
|
| 182 |
PROXY_VERSION,
|
| 183 |
+
_RedactingFilter,
|
| 184 |
_classify_token_type,
|
| 185 |
+
_mask_ip,
|
| 186 |
_resolve_upstream_url,
|
| 187 |
_safe_float,
|
| 188 |
_safe_int,
|
|
|
|
| 261 |
|
| 262 |
_handler = logging.StreamHandler()
|
| 263 |
_handler.setFormatter(_StructuredFormatter())
|
| 264 |
+
_handler.addFilter(_RedactingFilter()) # scrub tokens/IPs before emission
|
| 265 |
logging.root.handlers = [_handler]
|
| 266 |
logging.root.setLevel(logging.INFO)
|
| 267 |
logger = logging.getLogger(__name__)
|
|
|
|
| 1251 |
_contrib_rl[client_ip] = (count, window_start)
|
| 1252 |
if count > 5: # noqa: PLR2004
|
| 1253 |
logger.warning(
|
| 1254 |
+
json.dumps({"event": "contribute.ratelimit", "ip": _mask_ip(client_ip)})
|
| 1255 |
)
|
| 1256 |
raise HTTPException(
|
| 1257 |
status_code=429,
|
|
|
|
| 1360 |
) from exc
|
| 1361 |
|
| 1362 |
logger.info(
|
| 1363 |
+
json.dumps({"event": "contribute.write", "rows": len(records), "ip": _mask_ip(client_ip)})
|
| 1364 |
)
|
| 1365 |
return JSONResponse({"contributed": True, "rows": len(records)})
|
| 1366 |
|
|
|
|
| 1441 |
count += 1
|
| 1442 |
_share_rl[client_ip] = (count, window_start)
|
| 1443 |
if count > 10: # noqa: PLR2004
|
| 1444 |
+
logger.warning(json.dumps({"event": "share.ratelimit", "ip": _mask_ip(client_ip)}))
|
| 1445 |
raise HTTPException(
|
| 1446 |
status_code=429,
|
| 1447 |
detail="Rate limit exceeded. Maximum 10 shares per hour.",
|
|
|
|
| 1474 |
{
|
| 1475 |
"event": "share.create",
|
| 1476 |
"id": share_id,
|
| 1477 |
+
"ip": _mask_ip(client_ip),
|
| 1478 |
"ttl_days": ttl_days,
|
| 1479 |
}
|
| 1480 |
)
|
|
|
|
| 1618 |
count += 1
|
| 1619 |
_share_rl[client_ip] = (count, window_start)
|
| 1620 |
if count > 10: # noqa: PLR2004
|
| 1621 |
+
logger.warning(json.dumps({"event": "share.ratelimit", "ip": _mask_ip(client_ip)}))
|
| 1622 |
raise HTTPException(
|
| 1623 |
status_code=429,
|
| 1624 |
detail="Rate limit exceeded. Maximum 10 shares per hour.",
|
|
|
|
| 1665 |
{
|
| 1666 |
"event": "share.update",
|
| 1667 |
"id": share_id,
|
| 1668 |
+
"ip": _mask_ip(client_ip),
|
| 1669 |
"ttl_days": ttl_days,
|
| 1670 |
}
|
| 1671 |
)
|
|
|
|
| 1783 |
json.dumps(
|
| 1784 |
{
|
| 1785 |
"event": "feedback.retract",
|
| 1786 |
+
"ip": _mask_ip(client_ip),
|
| 1787 |
"prevSessionId": payload.get("prevSessionId"),
|
| 1788 |
"conversationId": payload.get("conversationId"),
|
| 1789 |
"answerIndex": payload.get("answerIndex"),
|
|
|
|
| 1812 |
_feedback_rl[client_ip] = (count, window_start)
|
| 1813 |
if count > 30: # noqa: PLR2004
|
| 1814 |
logger.warning(
|
| 1815 |
+
json.dumps({"event": "feedback.ratelimit", "ip": _mask_ip(client_ip)})
|
| 1816 |
)
|
| 1817 |
raise HTTPException(
|
| 1818 |
status_code=429,
|
|
|
|
| 1824 |
json.dumps(
|
| 1825 |
{
|
| 1826 |
"event": "feedback.receive",
|
| 1827 |
+
"ip": _mask_ip(client_ip),
|
| 1828 |
"ratingValue": payload.get("ratingValue"),
|
| 1829 |
"model": payload.get("model"),
|
| 1830 |
"conversationId": payload.get("conversationId"),
|
|
|
|
| 1903 |
"event": "feedback.persist_ok",
|
| 1904 |
"retract": is_retract,
|
| 1905 |
"dedup_key": f"{conversation_id}:{answer_index}",
|
| 1906 |
+
"ip": _mask_ip(client_ip),
|
| 1907 |
}
|
| 1908 |
)
|
| 1909 |
)
|
deduplicate_dataset.py
CHANGED
|
@@ -59,12 +59,22 @@ from __future__ import annotations
|
|
| 59 |
import argparse
|
| 60 |
import json
|
| 61 |
import logging
|
|
|
|
| 62 |
import sys
|
| 63 |
from pathlib import Path
|
| 64 |
from typing import Any
|
| 65 |
|
| 66 |
logger = logging.getLogger(__name__)
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
# Optional: normalize records from v1 to v2 schema when _dataset_schema is
|
| 69 |
# available alongside this script (standard _hf_spaces_proxy/ deployment).
|
| 70 |
# Falls back to identity function with a warning for standalone usage.
|
|
@@ -359,6 +369,14 @@ def _configure_logging() -> None:
|
|
| 359 |
err_handler.setFormatter(level_fmt)
|
| 360 |
err_handler.setLevel(logging.WARNING)
|
| 361 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
root = logging.getLogger()
|
| 363 |
root.handlers = [out_handler, err_handler]
|
| 364 |
root.setLevel(logging.DEBUG)
|
|
@@ -416,13 +434,22 @@ def main(argv: list[str] | None = None) -> int:
|
|
| 416 |
)
|
| 417 |
return 1
|
| 418 |
logger.info("Downloading %s ...", args.repo_id)
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
|
|
|
|
|
|
| 424 |
)
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
|
| 427 |
logger.info("Reading records from %s ...", local_dir)
|
| 428 |
all_records = load_all_records(local_dir)
|
|
|
|
| 59 |
import argparse
|
| 60 |
import json
|
| 61 |
import logging
|
| 62 |
+
import re
|
| 63 |
import sys
|
| 64 |
from pathlib import Path
|
| 65 |
from typing import Any
|
| 66 |
|
| 67 |
logger = logging.getLogger(__name__)
|
| 68 |
|
| 69 |
+
# Optional: import _RedactingFilter from _shared_logic when available so that
|
| 70 |
+
# HF token strings embedded in exception messages (e.g. snapshot_download auth
|
| 71 |
+
# failures) are scrubbed from CLI log output. Safe no-op fallback for
|
| 72 |
+
# standalone usage where _shared_logic.py is absent.
|
| 73 |
+
try:
|
| 74 |
+
from _shared_logic import _RedactingFilter as _REDACTING_FILTER_CLS
|
| 75 |
+
except ImportError:
|
| 76 |
+
_REDACTING_FILTER_CLS = None # type: ignore[assignment,misc]
|
| 77 |
+
|
| 78 |
# Optional: normalize records from v1 to v2 schema when _dataset_schema is
|
| 79 |
# available alongside this script (standard _hf_spaces_proxy/ deployment).
|
| 80 |
# Falls back to identity function with a warning for standalone usage.
|
|
|
|
| 369 |
err_handler.setFormatter(level_fmt)
|
| 370 |
err_handler.setLevel(logging.WARNING)
|
| 371 |
|
| 372 |
+
# Attach defence-in-depth redaction filter when _shared_logic is available.
|
| 373 |
+
# Scrubs HF token strings that huggingface_hub may embed in auth-error
|
| 374 |
+
# messages before they are emitted to stderr. No-op when absent.
|
| 375 |
+
if _REDACTING_FILTER_CLS is not None:
|
| 376 |
+
_rf = _REDACTING_FILTER_CLS()
|
| 377 |
+
out_handler.addFilter(_rf)
|
| 378 |
+
err_handler.addFilter(_rf)
|
| 379 |
+
|
| 380 |
root = logging.getLogger()
|
| 381 |
root.handlers = [out_handler, err_handler]
|
| 382 |
root.setLevel(logging.DEBUG)
|
|
|
|
| 434 |
)
|
| 435 |
return 1
|
| 436 |
logger.info("Downloading %s ...", args.repo_id)
|
| 437 |
+
try:
|
| 438 |
+
local_dir = Path(
|
| 439 |
+
snapshot_download(
|
| 440 |
+
repo_id=args.repo_id,
|
| 441 |
+
repo_type="dataset",
|
| 442 |
+
token=args.token,
|
| 443 |
+
)
|
| 444 |
)
|
| 445 |
+
except Exception as exc: # noqa: BLE001
|
| 446 |
+
logger.error(
|
| 447 |
+
"Failed to download %s: %s\n"
|
| 448 |
+
"Hint: pass --token <HF_READ_TOKEN> or set HF_TOKEN in your environment.",
|
| 449 |
+
args.repo_id,
|
| 450 |
+
exc,
|
| 451 |
+
)
|
| 452 |
+
return 1
|
| 453 |
|
| 454 |
logger.info("Reading records from %s ...", local_dir)
|
| 455 |
all_records = load_all_records(local_dir)
|