wchen22 commited on
Commit
a7250d0
·
verified ·
1 Parent(s): 3040495

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +3 -0
  2. __pycache__/app.cpython-314.pyc +0 -0
  3. app.py +181 -3
README.md CHANGED
@@ -47,6 +47,9 @@ Live Space:
47
  returned KEEP-only DeBERTa tokenizer labels. Receipts include
48
  removed-span/char totals, classifier DROP block reasons, and tool-schema
49
  preservation counts when `tools` or `tool_schemas` are supplied.
 
 
 
50
  The HTTP surface accepts `Content-Encoding: gzip` request bodies and gzip
51
  responses for `Accept-Encoding: gzip` or gzipped requests. If an ingress
52
  strips the standard content-encoding header, also send
 
47
  returned KEEP-only DeBERTa tokenizer labels. Receipts include
48
  removed-span/char totals, classifier DROP block reasons, and tool-schema
49
  preservation counts when `tools` or `tool_schemas` are supplied.
50
+ Matching `Idempotency-Key` retries replay the first in-memory response;
51
+ payload conflicts return HTTP 409. This is per-process memory on the Space,
52
+ not a durable distributed store.
53
  The HTTP surface accepts `Content-Encoding: gzip` request bodies and gzip
54
  responses for `Accept-Encoding: gzip` or gzipped requests. If an ingress
55
  strips the standard content-encoding header, also send
__pycache__/app.cpython-314.pyc ADDED
Binary file (62 kB). View file
 
app.py CHANGED
@@ -1,11 +1,13 @@
1
  from __future__ import annotations
2
 
 
3
  import gzip
4
  import hashlib
5
  import json
6
  import math
7
  import os
8
  import re
 
9
  import time
10
  import uuid
11
  from functools import lru_cache
@@ -25,6 +27,8 @@ TOUCHDOWN_CONTENT_ENCODING_HEADER = "X-Touchdown-Content-Encoding"
25
  ACCEPT_ENCODING_HEADER = "Accept-Encoding"
26
  GZIP_ENCODING = "gzip"
27
  GZIP_MAGIC = b"\x1f\x8b"
 
 
28
  LOW_SIGNAL_PATTERNS = [
29
  re.compile(pattern, re.IGNORECASE)
30
  for pattern in [
@@ -116,6 +120,10 @@ class UnsupportedContentEncoding(ValueError):
116
  """Raised when an HTTP request uses an unsupported content encoding."""
117
 
118
 
 
 
 
 
119
  def _content_encoding(value: str | None) -> str:
120
  return (value or "identity").split(";", 1)[0].strip().lower() or "identity"
121
 
@@ -156,6 +164,178 @@ def _should_gzip_response(
156
  return request_was_gzip or _accepts_gzip(accept_encoding)
157
 
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  async def _gzip_response_if_needed(
160
  request: Request,
161
  response: Response,
@@ -1007,6 +1187,4 @@ async def compress(request: Request) -> dict[str, Any]:
1007
  request_id=getattr(request.state, "request_id", None),
1008
  idempotency_key=request.headers.get(IDEMPOTENCY_KEY_HEADER),
1009
  )
1010
- if "inputs" in payload:
1011
- return _handle_batch(payload)
1012
- return _compress_text(payload)
 
1
  from __future__ import annotations
2
 
3
+ import copy
4
  import gzip
5
  import hashlib
6
  import json
7
  import math
8
  import os
9
  import re
10
+ import threading
11
  import time
12
  import uuid
13
  from functools import lru_cache
 
27
  ACCEPT_ENCODING_HEADER = "Accept-Encoding"
28
  GZIP_ENCODING = "gzip"
29
  GZIP_MAGIC = b"\x1f\x8b"
30
+ DEFAULT_IDEMPOTENCY_TTL_SECONDS = 24 * 60 * 60
31
+ IDEMPOTENCY_TTL_ENV = "TOUCHDOWN_IDEMPOTENCY_TTL_SECONDS"
32
  LOW_SIGNAL_PATTERNS = [
33
  re.compile(pattern, re.IGNORECASE)
34
  for pattern in [
 
120
  """Raised when an HTTP request uses an unsupported content encoding."""
121
 
122
 
123
+ _IDEMPOTENCY_LOCK = threading.Lock()
124
+ _IDEMPOTENCY_CACHE: dict[tuple[str, str, str], dict[str, Any]] = {}
125
+
126
+
127
  def _content_encoding(value: str | None) -> str:
128
  return (value or "identity").split(";", 1)[0].strip().lower() or "identity"
129
 
 
164
  return request_was_gzip or _accepts_gzip(accept_encoding)
165
 
166
 
167
+ def _idempotency_ttl_seconds() -> int:
168
+ raw = os.environ.get(IDEMPOTENCY_TTL_ENV)
169
+ if raw is None:
170
+ return DEFAULT_IDEMPOTENCY_TTL_SECONDS
171
+ try:
172
+ parsed = int(raw)
173
+ except ValueError:
174
+ return DEFAULT_IDEMPOTENCY_TTL_SECONDS
175
+ return max(0, parsed)
176
+
177
+
178
+ def _idempotency_key_from_payload(payload: dict[str, Any]) -> str | None:
179
+ value = payload.get("idempotency_key")
180
+ return value if isinstance(value, str) and value else None
181
+
182
+
183
+ def _fingerprint_value(value: Any) -> Any:
184
+ if isinstance(value, dict):
185
+ return {
186
+ key: _fingerprint_value(item)
187
+ for key, item in sorted(value.items())
188
+ if key not in {"request_id", "idempotency_key"}
189
+ }
190
+ if isinstance(value, list):
191
+ return [_fingerprint_value(item) for item in value]
192
+ return value
193
+
194
+
195
+ def _idempotency_fingerprint(route: str, payload: dict[str, Any]) -> str:
196
+ encoded = json.dumps(
197
+ {
198
+ "route": route,
199
+ "payload": _fingerprint_value(payload),
200
+ },
201
+ ensure_ascii=False,
202
+ sort_keys=True,
203
+ separators=(",", ":"),
204
+ )
205
+ return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
206
+
207
+
208
+ def _walk_receipts(value: Any) -> list[dict[str, Any]]:
209
+ receipts: list[dict[str, Any]] = []
210
+ if isinstance(value, dict):
211
+ receipt = value.get("receipt")
212
+ if isinstance(receipt, dict):
213
+ receipts.append(receipt)
214
+ for item in value.values():
215
+ receipts.extend(_walk_receipts(item))
216
+ elif isinstance(value, list):
217
+ for item in value:
218
+ receipts.extend(_walk_receipts(item))
219
+ return receipts
220
+
221
+
222
+ def _mark_idempotency(
223
+ body: dict[str, Any],
224
+ *,
225
+ key: str,
226
+ fingerprint: str,
227
+ replayed: bool,
228
+ original_request_id: str | None,
229
+ ) -> dict[str, Any]:
230
+ ttl_seconds = _idempotency_ttl_seconds()
231
+ marked = copy.deepcopy(body)
232
+ marked["idempotency_key"] = key
233
+ marked["idempotency_mode"] = "in_memory_response_replay"
234
+ marked["idempotency_replayed"] = replayed
235
+ marked["idempotency_fingerprint"] = fingerprint
236
+ marked["idempotency_ttl_seconds"] = ttl_seconds
237
+ if original_request_id:
238
+ marked["idempotency_original_request_id"] = original_request_id
239
+ for receipt in _walk_receipts(marked):
240
+ if receipt.get("idempotency_key") in (None, key):
241
+ receipt["idempotency_key"] = key
242
+ receipt["idempotency_mode"] = "in_memory_response_replay"
243
+ receipt["idempotency_replayed"] = replayed
244
+ receipt["idempotency_fingerprint"] = fingerprint
245
+ receipt["idempotency_ttl_seconds"] = ttl_seconds
246
+ if original_request_id:
247
+ receipt["idempotency_original_request_id"] = original_request_id
248
+ return marked
249
+
250
+
251
+ def _cached_idempotency_body(
252
+ *,
253
+ route: str,
254
+ key: str,
255
+ fingerprint: str,
256
+ ) -> dict[str, Any] | None:
257
+ now = time.monotonic()
258
+ with _IDEMPOTENCY_LOCK:
259
+ expired = [
260
+ cache_key for cache_key, record in _IDEMPOTENCY_CACHE.items()
261
+ if float(record["expires_at"]) <= now
262
+ ]
263
+ for cache_key in expired:
264
+ _IDEMPOTENCY_CACHE.pop(cache_key, None)
265
+ prefix = (route, key)
266
+ for cache_key, record in _IDEMPOTENCY_CACHE.items():
267
+ if cache_key[:2] != prefix:
268
+ continue
269
+ if record["fingerprint"] != fingerprint:
270
+ raise HTTPException(status_code=409, detail={
271
+ "schema_version": API_SCHEMA_VERSION,
272
+ "status": "error",
273
+ "error": "idempotency_key_reused_with_different_payload",
274
+ "idempotency_key": key,
275
+ "idempotency_fingerprint": fingerprint,
276
+ "idempotency_existing_fingerprint": record["fingerprint"],
277
+ })
278
+ return _mark_idempotency(
279
+ record["body"],
280
+ key=key,
281
+ fingerprint=fingerprint,
282
+ replayed=True,
283
+ original_request_id=record.get("request_id"),
284
+ )
285
+ return None
286
+
287
+
288
+ def _store_idempotency_body(
289
+ *,
290
+ route: str,
291
+ key: str,
292
+ fingerprint: str,
293
+ body: dict[str, Any],
294
+ request_id: str | None,
295
+ ) -> dict[str, Any]:
296
+ marked = _mark_idempotency(
297
+ body,
298
+ key=key,
299
+ fingerprint=fingerprint,
300
+ replayed=False,
301
+ original_request_id=request_id,
302
+ )
303
+ ttl_seconds = _idempotency_ttl_seconds()
304
+ if ttl_seconds > 0:
305
+ with _IDEMPOTENCY_LOCK:
306
+ _IDEMPOTENCY_CACHE[(route, key, fingerprint)] = {
307
+ "fingerprint": fingerprint,
308
+ "body": marked,
309
+ "request_id": request_id,
310
+ "expires_at": time.monotonic() + ttl_seconds,
311
+ }
312
+ return marked
313
+
314
+
315
+ def _handle_compress_with_idempotency(payload: dict[str, Any]) -> dict[str, Any]:
316
+ key = _idempotency_key_from_payload(payload)
317
+ if not key:
318
+ return _handle_batch(payload) if "inputs" in payload else _compress_text(payload)
319
+ route = "/v1/compress"
320
+ fingerprint = _idempotency_fingerprint(route, payload)
321
+ cached = _cached_idempotency_body(
322
+ route=route,
323
+ key=key,
324
+ fingerprint=fingerprint,
325
+ )
326
+ if cached is not None:
327
+ return cached
328
+ body = _handle_batch(payload) if "inputs" in payload else _compress_text(payload)
329
+ request_id = payload.get("request_id") if isinstance(payload.get("request_id"), str) else None
330
+ return _store_idempotency_body(
331
+ route=route,
332
+ key=key,
333
+ fingerprint=fingerprint,
334
+ body=body,
335
+ request_id=request_id,
336
+ )
337
+
338
+
339
  async def _gzip_response_if_needed(
340
  request: Request,
341
  response: Response,
 
1187
  request_id=getattr(request.state, "request_id", None),
1188
  idempotency_key=request.headers.get(IDEMPOTENCY_KEY_HEADER),
1189
  )
1190
+ return _handle_compress_with_idempotency(payload)