hypeline 0.1.1__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 (79) hide show
  1. hypeline-0.1.1/PKG-INFO +33 -0
  2. hypeline-0.1.1/README.md +19 -0
  3. hypeline-0.1.1/hypeline/__init__.py +190 -0
  4. hypeline-0.1.1/hypeline/stream.py +192 -0
  5. hypeline-0.1.1/hypeline/webhook.py +43 -0
  6. hypeline-0.1.1/hypeline.egg-info/PKG-INFO +33 -0
  7. hypeline-0.1.1/hypeline.egg-info/SOURCES.txt +77 -0
  8. hypeline-0.1.1/hypeline.egg-info/dependency_links.txt +1 -0
  9. hypeline-0.1.1/hypeline.egg-info/requires.txt +5 -0
  10. hypeline-0.1.1/hypeline.egg-info/top_level.txt +2 -0
  11. hypeline-0.1.1/hypeline_core/__init__.py +84 -0
  12. hypeline-0.1.1/hypeline_core/api/__init__.py +11 -0
  13. hypeline-0.1.1/hypeline_core/api/event_kinds_api.py +285 -0
  14. hypeline-0.1.1/hypeline_core/api/history_api.py +816 -0
  15. hypeline-0.1.1/hypeline_core/api/render_api.py +313 -0
  16. hypeline-0.1.1/hypeline_core/api/sources_api.py +2494 -0
  17. hypeline-0.1.1/hypeline_core/api/stream_api.py +391 -0
  18. hypeline-0.1.1/hypeline_core/api/templates_api.py +1662 -0
  19. hypeline-0.1.1/hypeline_core/api/transparency_log_api.py +634 -0
  20. hypeline-0.1.1/hypeline_core/api_client.py +797 -0
  21. hypeline-0.1.1/hypeline_core/api_response.py +21 -0
  22. hypeline-0.1.1/hypeline_core/configuration.py +603 -0
  23. hypeline-0.1.1/hypeline_core/exceptions.py +199 -0
  24. hypeline-0.1.1/hypeline_core/models/__init__.py +61 -0
  25. hypeline-0.1.1/hypeline_core/models/alert_result.py +95 -0
  26. hypeline-0.1.1/hypeline_core/models/alert_spec.py +98 -0
  27. hypeline-0.1.1/hypeline_core/models/content_diff.py +116 -0
  28. hypeline-0.1.1/hypeline_core/models/copy.py +94 -0
  29. hypeline-0.1.1/hypeline_core/models/create_source_request.py +141 -0
  30. hypeline-0.1.1/hypeline_core/models/delivery.py +94 -0
  31. hypeline-0.1.1/hypeline_core/models/delta_op.py +95 -0
  32. hypeline-0.1.1/hypeline_core/models/delta_stats.py +93 -0
  33. hypeline-0.1.1/hypeline_core/models/detect_dto.py +99 -0
  34. hypeline-0.1.1/hypeline_core/models/detect_source_request.py +91 -0
  35. hypeline-0.1.1/hypeline_core/models/diff_segment.py +89 -0
  36. hypeline-0.1.1/hypeline_core/models/error_detail.py +96 -0
  37. hypeline-0.1.1/hypeline_core/models/error_model.py +114 -0
  38. hypeline-0.1.1/hypeline_core/models/event_kind_dto.py +102 -0
  39. hypeline-0.1.1/hypeline_core/models/health_dto.py +102 -0
  40. hypeline-0.1.1/hypeline_core/models/history_page.py +115 -0
  41. hypeline-0.1.1/hypeline_core/models/inclusion.py +95 -0
  42. hypeline-0.1.1/hypeline_core/models/instantiate_adhoc_template_request.py +110 -0
  43. hypeline-0.1.1/hypeline_core/models/instantiate_template_request.py +95 -0
  44. hypeline-0.1.1/hypeline_core/models/manifest.py +158 -0
  45. hypeline-0.1.1/hypeline_core/models/next_steps.py +89 -0
  46. hypeline-0.1.1/hypeline_core/models/parameter.py +101 -0
  47. hypeline-0.1.1/hypeline_core/models/pdf_page_span.py +91 -0
  48. hypeline-0.1.1/hypeline_core/models/picker_box.py +99 -0
  49. hypeline-0.1.1/hypeline_core/models/picker_dto.py +114 -0
  50. hypeline-0.1.1/hypeline_core/models/preview_row_dto.py +95 -0
  51. hypeline-0.1.1/hypeline_core/models/preview_template_request.py +91 -0
  52. hypeline-0.1.1/hypeline_core/models/receipt.py +115 -0
  53. hypeline-0.1.1/hypeline_core/models/render_doc_body.py +123 -0
  54. hypeline-0.1.1/hypeline_core/models/render_doc_viewport.py +93 -0
  55. hypeline-0.1.1/hypeline_core/models/render_doc_wait.py +89 -0
  56. hypeline-0.1.1/hypeline_core/models/render_multi_response.py +97 -0
  57. hypeline-0.1.1/hypeline_core/models/report.py +137 -0
  58. hypeline-0.1.1/hypeline_core/models/signed_tree_head.py +98 -0
  59. hypeline-0.1.1/hypeline_core/models/source_dto.py +160 -0
  60. hypeline-0.1.1/hypeline_core/models/source_leaf_entry.py +100 -0
  61. hypeline-0.1.1/hypeline_core/models/source_log_page.py +114 -0
  62. hypeline-0.1.1/hypeline_core/models/source_result.py +95 -0
  63. hypeline-0.1.1/hypeline_core/models/source_spec.py +106 -0
  64. hypeline-0.1.1/hypeline_core/models/stream_event.py +192 -0
  65. hypeline-0.1.1/hypeline_core/models/summary.py +93 -0
  66. hypeline-0.1.1/hypeline_core/models/template_summary_dto.py +104 -0
  67. hypeline-0.1.1/hypeline_core/models/text_delta.py +112 -0
  68. hypeline-0.1.1/hypeline_core/models/typed_payload.py +100 -0
  69. hypeline-0.1.1/hypeline_core/models/update_source_request.py +141 -0
  70. hypeline-0.1.1/hypeline_core/py.typed +0 -0
  71. hypeline-0.1.1/hypeline_core/rest.py +257 -0
  72. hypeline-0.1.1/pyproject.toml +26 -0
  73. hypeline-0.1.1/setup.cfg +4 -0
  74. hypeline-0.1.1/tests/test_changes.py +33 -0
  75. hypeline-0.1.1/tests/test_event_shape.py +27 -0
  76. hypeline-0.1.1/tests/test_stream.py +95 -0
  77. hypeline-0.1.1/tests/test_surface.py +31 -0
  78. hypeline-0.1.1/tests/test_watch.py +62 -0
  79. hypeline-0.1.1/tests/test_webhook.py +61 -0
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: hypeline
3
+ Version: 0.1.1
4
+ Summary: Official Hypeline SDK: turn any source into a unified, deduplicated live stream of change events. Watches, change history, SSE with gap-free resume, and Standard Webhooks signature verification.
5
+ License: MIT
6
+ Keywords: hypeline,monitoring,webhooks,sse,change-detection,agents
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: urllib3<3.0.0,>=1.25.3
10
+ Requires-Dist: python_dateutil>=2.8.2
11
+ Requires-Dist: pydantic>=2
12
+ Requires-Dist: typing-extensions>=4.7.1
13
+ Requires-Dist: standardwebhooks>=1.0.0
14
+
15
+ # hypeline
16
+
17
+ Official Python SDK for Hypeline. Create watches on any source URL, poll change
18
+ history, subscribe to the live event stream over SSE (gap-free resume), and verify
19
+ signed webhooks.
20
+
21
+ ```python
22
+ from hypeline import Hypeline
23
+
24
+ hy = Hypeline(api_key="...")
25
+ watch = hy.create_watch("https://example.com/blog", keywords=["incident"])
26
+ for change in hy.get_changes_since(watch.cursor).events:
27
+ print(change.content)
28
+
29
+ for ev in hy.stream(keyword="incident"):
30
+ print(ev.emitted_at, ev.content)
31
+ ```
32
+
33
+ See `sdk/README.md` for the shared regen/drift workflow.
@@ -0,0 +1,19 @@
1
+ # hypeline
2
+
3
+ Official Python SDK for Hypeline. Create watches on any source URL, poll change
4
+ history, subscribe to the live event stream over SSE (gap-free resume), and verify
5
+ signed webhooks.
6
+
7
+ ```python
8
+ from hypeline import Hypeline
9
+
10
+ hy = Hypeline(api_key="...")
11
+ watch = hy.create_watch("https://example.com/blog", keywords=["incident"])
12
+ for change in hy.get_changes_since(watch.cursor).events:
13
+ print(change.content)
14
+
15
+ for ev in hy.stream(keyword="incident"):
16
+ print(ev.emitted_at, ev.content)
17
+ ```
18
+
19
+ See `sdk/README.md` for the shared regen/drift workflow.
@@ -0,0 +1,190 @@
1
+ """hypeline: the official Python SDK.
2
+
3
+ Wraps the generated core (``hypeline_core``, openapi-generator) and re-exposes exactly
4
+ the marketed snake_case surface:
5
+ create_watch, list_watches, delete_watch, get_changes_since, stream, verify_signature
6
+
7
+ The event model mirrors the REST/SSE wire (``content`` + ``emitted_at``, ``simhash`` a
8
+ string), NOT the MCP projection (``summary``/``occurred_at``).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import random
13
+ import time
14
+ from dataclasses import dataclass
15
+ from typing import Any, Iterator, List, Optional
16
+
17
+ from hypeline_core.api.history_api import HistoryApi
18
+ from hypeline_core.api.sources_api import SourcesApi
19
+ from hypeline_core.api_client import ApiClient
20
+ from hypeline_core.configuration import Configuration
21
+ from hypeline_core.models.create_source_request import CreateSourceRequest
22
+ from hypeline_core.models.history_page import HistoryPage
23
+ from hypeline_core.models.source_dto import SourceDTO
24
+ from hypeline_core.models.stream_event import StreamEvent
25
+
26
+ from .stream import CursorExpired, stream_events
27
+ from .webhook import verify_signature
28
+
29
+ __all__ = [
30
+ "Hypeline",
31
+ "Watch",
32
+ "StreamEvent",
33
+ "SourceDTO",
34
+ "HistoryPage",
35
+ "CursorExpired",
36
+ "verify_signature",
37
+ ]
38
+
39
+ DEFAULT_BASE_URL = "https://api.hypeline.io"
40
+
41
+
42
+ @dataclass
43
+ class Watch:
44
+ """A created watch: the source id plus a head cursor to poll get_changes_since from."""
45
+
46
+ id: str
47
+ cursor: str
48
+ source: SourceDTO
49
+
50
+
51
+ class Hypeline:
52
+ """The idiomatic Hypeline client."""
53
+
54
+ def __init__(
55
+ self,
56
+ api_key: Optional[str] = None,
57
+ access_token: Optional[str] = None,
58
+ base_url: str = DEFAULT_BASE_URL,
59
+ ) -> None:
60
+ self._base_url = base_url.rstrip("/")
61
+ self._api_key = api_key
62
+ self._access_token = access_token
63
+ config = Configuration(host=self._base_url)
64
+ if api_key:
65
+ # Auth scheme name from the overlay: ApiKeyAuth -> header X-API-Key.
66
+ config.api_key = {"ApiKeyAuth": api_key}
67
+ if access_token:
68
+ config.access_token = access_token
69
+ self._client = ApiClient(config)
70
+ self._sources = SourcesApi(self._client)
71
+ self._history = HistoryApi(self._client)
72
+
73
+ # --- watches ---------------------------------------------------------------
74
+
75
+ def create_watch(
76
+ self,
77
+ url: str,
78
+ keywords: Optional[List[str]] = None,
79
+ type: Optional[str] = None,
80
+ ) -> Watch:
81
+ """Create a watch on a source URL. ``keywords`` are stored as source ``tags``.
82
+
83
+ Returns the watch id plus a head cursor (the current newest event id) so the
84
+ first ``get_changes_since(cursor)`` receives only changes after creation.
85
+ """
86
+ req = CreateSourceRequest(
87
+ url=url,
88
+ tags=keywords if keywords else None,
89
+ type=type,
90
+ )
91
+ source = self._sources.create_source(create_source_request=req)
92
+ return Watch(id=source.id, cursor=self._head_cursor(), source=source)
93
+
94
+ def list_watches(self) -> List[SourceDTO]:
95
+ """List all watches (sources) for the authenticated key."""
96
+ return self._sources.list_sources()
97
+
98
+ def delete_watch(self, watch_id: str) -> None:
99
+ """Delete a watch by id."""
100
+ self._sources.delete_source(id=watch_id)
101
+
102
+ # --- changes ---------------------------------------------------------------
103
+
104
+ def get_changes_since(
105
+ self,
106
+ cursor: Optional[str] = None,
107
+ limit: Optional[int] = None,
108
+ source: Optional[Any] = None,
109
+ keyword: Optional[Any] = None,
110
+ ) -> HistoryPage:
111
+ """Fetch changes strictly after ``cursor`` (oldest-first).
112
+
113
+ Returns a ``HistoryPage`` whose ``.events`` are typed ``StreamEvent`` objects and
114
+ whose ``.next_cursor`` pages the next call (``None`` once caught up).
115
+ """
116
+ return self._history.query_events(
117
+ var_from=cursor,
118
+ order="asc",
119
+ limit=limit,
120
+ source=_as_list(source),
121
+ keyword=_as_list(keyword),
122
+ )
123
+
124
+ # --- live stream -----------------------------------------------------------
125
+
126
+ def stream(
127
+ self,
128
+ keyword: Optional[str] = None,
129
+ source: Optional[Any] = None,
130
+ source_type: Optional[str] = None,
131
+ tag: Optional[Any] = None,
132
+ since: Optional[str] = None,
133
+ ) -> Iterator[StreamEvent]:
134
+ """Subscribe to the live stream (SSE), resuming gap-free on disconnect.
135
+
136
+ The filter is a SINGULAR ``keyword`` string (mapped to ``?keyword=``); there is
137
+ no plural ``keywords`` list here (that belongs to ``create_watch`` -> ``tags``).
138
+ A 410 raises :class:`CursorExpired`.
139
+ """
140
+ return stream_events(
141
+ self._base_url,
142
+ api_key=self._api_key,
143
+ access_token=self._access_token,
144
+ keyword=keyword,
145
+ source=source,
146
+ source_type=source_type,
147
+ tag=tag,
148
+ since=since,
149
+ )
150
+
151
+ # --- webhooks --------------------------------------------------------------
152
+
153
+ def verify_signature(self, secret: str, headers: dict, body: Any) -> Any:
154
+ """Verify a signed webhook delivery (see :func:`hypeline.verify_signature`)."""
155
+ return verify_signature(secret, headers, body)
156
+
157
+ # --- internals -------------------------------------------------------------
158
+
159
+ def _head_cursor(self) -> str:
160
+ """Current head event id (mirrors MCP headCursor: GET /v1/events?order=desc&limit=1).
161
+
162
+ On an empty log or any failure, synthesize a time-ordered cursor so polling still
163
+ starts from the present.
164
+ """
165
+ try:
166
+ page = self._history.query_events(order="desc", limit=1)
167
+ events = page.events or []
168
+ if events and events[0].id:
169
+ return events[0].id
170
+ except Exception: # noqa: BLE001
171
+ pass
172
+ return _synthesize_cursor()
173
+
174
+
175
+ def _as_list(val: Any) -> Optional[List[str]]:
176
+ if val is None:
177
+ return None
178
+ return list(val) if isinstance(val, (list, tuple)) else [val]
179
+
180
+
181
+ def _synthesize_cursor() -> str:
182
+ """A UUIDv7-ish time-ordered id used as a 'now' cursor fallback."""
183
+ ms = int(time.time() * 1000)
184
+ time_hex = f"{ms:012x}"
185
+ rand = f"{random.getrandbits(64):016x}"
186
+ var = random.choice("89ab")
187
+ return (
188
+ f"{time_hex[:8]}-{time_hex[8:12]}-7{rand[:3]}-"
189
+ f"{var}{rand[3:6]}-{rand[6:16]}{rand[:2]}"
190
+ )
@@ -0,0 +1,192 @@
1
+ """Hand-written Server-Sent Events resume shim over GET /v1/stream.
2
+
3
+ The generated core (openapi-generator python) models /v1/stream as a one-shot JSON
4
+ response, so it cannot iterate SSE frames. This shim owns the SSE wire directly
5
+ (matching internal/api/stream.go):
6
+
7
+ - frames: ``id: <uuidv7>`` (the resume cursor), ``event: <source_type>``, ``data: <json>``
8
+ - ``: keepalive`` comment frames are heartbeats and are skipped (never yielded)
9
+ - reconnect on disconnect sends the ``Last-Event-ID`` header and resumes gap-free
10
+ - a server ``retry:`` hint sets the reconnect backoff
11
+ - HTTP 410 Gone (a resume cursor past the retained window) raises ``CursorExpired``
12
+
13
+ The stream filter is a SINGULAR ``keyword`` string (mapped to ``?keyword=``), matching
14
+ the marketed ``/learn`` copy; the PLURAL ``keywords`` list belongs only to
15
+ ``create_watch`` (mapped to source ``tags``).
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import time
21
+ import urllib.error
22
+ import urllib.request
23
+ from typing import Any, Callable, Iterator, List, Optional
24
+ from urllib.parse import urlencode, urljoin
25
+
26
+ from hypeline_core.models.stream_event import StreamEvent
27
+
28
+
29
+ class CursorExpired(Exception):
30
+ """Raised when a resume cursor predates the retained backfill window (HTTP 410)."""
31
+
32
+ status = 410
33
+
34
+ def __init__(self, message: Optional[str] = None) -> None:
35
+ super().__init__(
36
+ message
37
+ or "stream cursor expired: the Last-Event-ID predates the retained window; "
38
+ "reconnect without it to resume from the live tail"
39
+ )
40
+
41
+
42
+ # A minimal opener signature: (url, headers) -> a file-like response with .read()/
43
+ # .readline() and a .status/.getcode(). Defaults to urllib.request.urlopen; injectable
44
+ # for tests that want to avoid a real socket (the suite uses a real localhost server).
45
+ Opener = Callable[[urllib.request.Request], Any]
46
+
47
+
48
+ def _build_url(
49
+ base_url: str,
50
+ source: Optional[Any],
51
+ source_type: Optional[str],
52
+ tag: Optional[Any],
53
+ keyword: Optional[str],
54
+ since: Optional[str],
55
+ ) -> str:
56
+ params: List[tuple] = []
57
+
58
+ def add(key: str, val: Any) -> None:
59
+ if val is None:
60
+ return
61
+ for v in val if isinstance(val, (list, tuple)) else [val]:
62
+ if v != "":
63
+ params.append((key, v))
64
+
65
+ add("source", source)
66
+ add("source_type", source_type)
67
+ add("tag", tag)
68
+ add("keyword", keyword) # SINGULAR; never a plural keywords list
69
+ add("since", since)
70
+ base = base_url if base_url.endswith("/") else base_url + "/"
71
+ url = urljoin(base, "v1/stream")
72
+ if params:
73
+ url = url + "?" + urlencode(params)
74
+ return url
75
+
76
+
77
+ def _parse_frame(raw: str):
78
+ """Parse a single SSE frame; returns (event_id, data_str, retry_ms, is_comment)."""
79
+ data_lines: List[str] = []
80
+ event_id: Optional[str] = None
81
+ retry_ms: Optional[int] = None
82
+ is_comment = False
83
+ for line in raw.split("\n"):
84
+ if line == "":
85
+ continue
86
+ if line.startswith(":"):
87
+ is_comment = True
88
+ continue
89
+ idx = line.find(":")
90
+ field = line if idx == -1 else line[:idx]
91
+ value = "" if idx == -1 else line[idx + 1 :]
92
+ if value.startswith(" "):
93
+ value = value[1:]
94
+ if field == "id":
95
+ event_id = value
96
+ elif field == "data":
97
+ data_lines.append(value)
98
+ elif field == "retry":
99
+ try:
100
+ retry_ms = int(value)
101
+ except ValueError:
102
+ pass
103
+ return event_id, "\n".join(data_lines), retry_ms, is_comment
104
+
105
+
106
+ def stream_events(
107
+ base_url: str,
108
+ api_key: Optional[str] = None,
109
+ access_token: Optional[str] = None,
110
+ *,
111
+ source: Optional[Any] = None,
112
+ source_type: Optional[str] = None,
113
+ tag: Optional[Any] = None,
114
+ keyword: Optional[str] = None,
115
+ since: Optional[str] = None,
116
+ opener: Optional[Opener] = None,
117
+ reconnect_delay: float = 1.0,
118
+ ) -> Iterator[StreamEvent]:
119
+ """Iterate the live stream, resuming gap-free across disconnects.
120
+
121
+ Yields typed ``StreamEvent`` objects. On a mid-stream disconnect it reconnects with
122
+ the ``Last-Event-ID`` header set to the last seen id, so no event is skipped or
123
+ repeated. A 410 raises :class:`CursorExpired` (terminal, no reconnect).
124
+ """
125
+ do_open: Opener = opener or (lambda req: urllib.request.urlopen(req))
126
+ url = _build_url(base_url, source, source_type, tag, keyword, since)
127
+ last_id: Optional[str] = since
128
+ retry = reconnect_delay
129
+
130
+ while True:
131
+ headers = {"Accept": "text/event-stream"}
132
+ if api_key:
133
+ headers["X-API-Key"] = api_key
134
+ if access_token:
135
+ headers["Authorization"] = f"Bearer {access_token}"
136
+ if last_id:
137
+ headers["Last-Event-ID"] = last_id
138
+
139
+ req = urllib.request.Request(url, headers=headers, method="GET")
140
+ try:
141
+ resp = do_open(req)
142
+ except urllib.error.HTTPError as err:
143
+ if err.code == 410:
144
+ raise CursorExpired() from None
145
+ if 400 <= err.code < 500:
146
+ raise
147
+ time.sleep(retry)
148
+ continue
149
+ except urllib.error.URLError:
150
+ time.sleep(retry)
151
+ continue
152
+
153
+ frame_lines: List[str] = []
154
+ disconnected = False
155
+ try:
156
+ while True:
157
+ raw_line = resp.readline()
158
+ if not raw_line:
159
+ disconnected = True
160
+ break
161
+ line = raw_line.decode("utf-8", errors="replace").rstrip("\n").rstrip("\r")
162
+ if line != "":
163
+ frame_lines.append(line)
164
+ continue
165
+ # Blank line: end of frame.
166
+ if not frame_lines:
167
+ continue
168
+ event_id, data, retry_ms, is_comment = _parse_frame("\n".join(frame_lines))
169
+ frame_lines = []
170
+ if retry_ms is not None:
171
+ retry = retry_ms / 1000.0
172
+ if event_id:
173
+ last_id = event_id
174
+ if is_comment or data == "":
175
+ continue
176
+ try:
177
+ payload = json.loads(data)
178
+ except json.JSONDecodeError:
179
+ continue
180
+ yield StreamEvent.from_dict(payload)
181
+ except (urllib.error.URLError, OSError):
182
+ disconnected = True
183
+ finally:
184
+ try:
185
+ resp.close()
186
+ except Exception: # noqa: BLE001
187
+ pass
188
+
189
+ if disconnected:
190
+ time.sleep(retry)
191
+ continue
192
+ return
@@ -0,0 +1,43 @@
1
+ """Webhook signature verification for Hypeline deliveries.
2
+
3
+ Hypeline signs webhooks with the Standard Webhooks scheme (the same scheme the engine
4
+ implements in internal/delivery/sign.go). We do NOT hand-roll HMAC here:
5
+ ``verify_signature`` wraps the official ``standardwebhooks`` library, which strips the
6
+ ``whsec_`` prefix, base64-decodes the secret, checks the three ``webhook-*`` headers,
7
+ enforces the 5-minute timestamp freshness window, and constant-time compares the v1
8
+ HMAC-SHA256 signature.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict, Union
13
+
14
+ from standardwebhooks import Webhook
15
+
16
+
17
+ def verify_signature(
18
+ secret: str,
19
+ headers: Dict[str, str],
20
+ body: Union[str, bytes],
21
+ ) -> Any:
22
+ """Verify a Hypeline webhook delivery.
23
+
24
+ Args:
25
+ secret: The endpoint's signing secret (the ``whsec_...`` value shown when the
26
+ webhook was created). Passed through to ``standardwebhooks`` verbatim.
27
+ headers: The delivery's HTTP headers. Must include ``webhook-id``,
28
+ ``webhook-timestamp`` and ``webhook-signature`` (header keys are normalized
29
+ to lower-case before verification).
30
+ body: The RAW request body (verify BEFORE parsing JSON: any re-serialization
31
+ changes bytes and breaks the signature).
32
+
33
+ Returns:
34
+ The parsed JSON payload when the signature and timestamp are valid.
35
+
36
+ Raises:
37
+ WebhookVerificationError: When the signature does not match, a required header
38
+ is missing, or the timestamp is outside the Standard Webhooks tolerance.
39
+ """
40
+ normalized = {str(k).lower(): v for k, v in headers.items()}
41
+ data = body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else body
42
+ wh = Webhook(secret)
43
+ return wh.verify(data, normalized)
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: hypeline
3
+ Version: 0.1.1
4
+ Summary: Official Hypeline SDK: turn any source into a unified, deduplicated live stream of change events. Watches, change history, SSE with gap-free resume, and Standard Webhooks signature verification.
5
+ License: MIT
6
+ Keywords: hypeline,monitoring,webhooks,sse,change-detection,agents
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: urllib3<3.0.0,>=1.25.3
10
+ Requires-Dist: python_dateutil>=2.8.2
11
+ Requires-Dist: pydantic>=2
12
+ Requires-Dist: typing-extensions>=4.7.1
13
+ Requires-Dist: standardwebhooks>=1.0.0
14
+
15
+ # hypeline
16
+
17
+ Official Python SDK for Hypeline. Create watches on any source URL, poll change
18
+ history, subscribe to the live event stream over SSE (gap-free resume), and verify
19
+ signed webhooks.
20
+
21
+ ```python
22
+ from hypeline import Hypeline
23
+
24
+ hy = Hypeline(api_key="...")
25
+ watch = hy.create_watch("https://example.com/blog", keywords=["incident"])
26
+ for change in hy.get_changes_since(watch.cursor).events:
27
+ print(change.content)
28
+
29
+ for ev in hy.stream(keyword="incident"):
30
+ print(ev.emitted_at, ev.content)
31
+ ```
32
+
33
+ See `sdk/README.md` for the shared regen/drift workflow.
@@ -0,0 +1,77 @@
1
+ README.md
2
+ pyproject.toml
3
+ hypeline/__init__.py
4
+ hypeline/stream.py
5
+ hypeline/webhook.py
6
+ hypeline.egg-info/PKG-INFO
7
+ hypeline.egg-info/SOURCES.txt
8
+ hypeline.egg-info/dependency_links.txt
9
+ hypeline.egg-info/requires.txt
10
+ hypeline.egg-info/top_level.txt
11
+ hypeline_core/__init__.py
12
+ hypeline_core/api_client.py
13
+ hypeline_core/api_response.py
14
+ hypeline_core/configuration.py
15
+ hypeline_core/exceptions.py
16
+ hypeline_core/py.typed
17
+ hypeline_core/rest.py
18
+ hypeline_core/api/__init__.py
19
+ hypeline_core/api/event_kinds_api.py
20
+ hypeline_core/api/history_api.py
21
+ hypeline_core/api/render_api.py
22
+ hypeline_core/api/sources_api.py
23
+ hypeline_core/api/stream_api.py
24
+ hypeline_core/api/templates_api.py
25
+ hypeline_core/api/transparency_log_api.py
26
+ hypeline_core/models/__init__.py
27
+ hypeline_core/models/alert_result.py
28
+ hypeline_core/models/alert_spec.py
29
+ hypeline_core/models/content_diff.py
30
+ hypeline_core/models/copy.py
31
+ hypeline_core/models/create_source_request.py
32
+ hypeline_core/models/delivery.py
33
+ hypeline_core/models/delta_op.py
34
+ hypeline_core/models/delta_stats.py
35
+ hypeline_core/models/detect_dto.py
36
+ hypeline_core/models/detect_source_request.py
37
+ hypeline_core/models/diff_segment.py
38
+ hypeline_core/models/error_detail.py
39
+ hypeline_core/models/error_model.py
40
+ hypeline_core/models/event_kind_dto.py
41
+ hypeline_core/models/health_dto.py
42
+ hypeline_core/models/history_page.py
43
+ hypeline_core/models/inclusion.py
44
+ hypeline_core/models/instantiate_adhoc_template_request.py
45
+ hypeline_core/models/instantiate_template_request.py
46
+ hypeline_core/models/manifest.py
47
+ hypeline_core/models/next_steps.py
48
+ hypeline_core/models/parameter.py
49
+ hypeline_core/models/pdf_page_span.py
50
+ hypeline_core/models/picker_box.py
51
+ hypeline_core/models/picker_dto.py
52
+ hypeline_core/models/preview_row_dto.py
53
+ hypeline_core/models/preview_template_request.py
54
+ hypeline_core/models/receipt.py
55
+ hypeline_core/models/render_doc_body.py
56
+ hypeline_core/models/render_doc_viewport.py
57
+ hypeline_core/models/render_doc_wait.py
58
+ hypeline_core/models/render_multi_response.py
59
+ hypeline_core/models/report.py
60
+ hypeline_core/models/signed_tree_head.py
61
+ hypeline_core/models/source_dto.py
62
+ hypeline_core/models/source_leaf_entry.py
63
+ hypeline_core/models/source_log_page.py
64
+ hypeline_core/models/source_result.py
65
+ hypeline_core/models/source_spec.py
66
+ hypeline_core/models/stream_event.py
67
+ hypeline_core/models/summary.py
68
+ hypeline_core/models/template_summary_dto.py
69
+ hypeline_core/models/text_delta.py
70
+ hypeline_core/models/typed_payload.py
71
+ hypeline_core/models/update_source_request.py
72
+ tests/test_changes.py
73
+ tests/test_event_shape.py
74
+ tests/test_stream.py
75
+ tests/test_surface.py
76
+ tests/test_watch.py
77
+ tests/test_webhook.py
@@ -0,0 +1,5 @@
1
+ urllib3<3.0.0,>=1.25.3
2
+ python_dateutil>=2.8.2
3
+ pydantic>=2
4
+ typing-extensions>=4.7.1
5
+ standardwebhooks>=1.0.0
@@ -0,0 +1,2 @@
1
+ hypeline
2
+ hypeline_core
@@ -0,0 +1,84 @@
1
+ # coding: utf-8
2
+
3
+ # flake8: noqa
4
+
5
+ """
6
+ Hypeline Stream API
7
+
8
+ Subscribe to Hypeline's unified, deduplicated live event stream over Server-Sent Events. Latency is honest and per-source-tier: push sources arrive in seconds, RSS/JSON feeds in minutes, and feed-less HTML-diff sources in minutes to tens of minutes. Resume gap-free with the Last-Event-ID cursor; a cursor older than the retained window returns 410 Gone.
9
+
10
+ The version of the OpenAPI document: 1.0.0
11
+ Generated by OpenAPI Generator (https://openapi-generator.tech)
12
+
13
+ Do not edit the class manually.
14
+ """ # noqa: E501
15
+
16
+
17
+ __version__ = "1.0.0"
18
+
19
+ # import apis into sdk package
20
+ from hypeline_core.api.event_kinds_api import EventKindsApi
21
+ from hypeline_core.api.history_api import HistoryApi
22
+ from hypeline_core.api.render_api import RenderApi
23
+ from hypeline_core.api.sources_api import SourcesApi
24
+ from hypeline_core.api.stream_api import StreamApi
25
+ from hypeline_core.api.templates_api import TemplatesApi
26
+ from hypeline_core.api.transparency_log_api import TransparencyLogApi
27
+
28
+ # import ApiClient
29
+ from hypeline_core.api_response import ApiResponse
30
+ from hypeline_core.api_client import ApiClient
31
+ from hypeline_core.configuration import Configuration
32
+ from hypeline_core.exceptions import OpenApiException
33
+ from hypeline_core.exceptions import ApiTypeError
34
+ from hypeline_core.exceptions import ApiValueError
35
+ from hypeline_core.exceptions import ApiKeyError
36
+ from hypeline_core.exceptions import ApiAttributeError
37
+ from hypeline_core.exceptions import ApiException
38
+
39
+ # import models into sdk package
40
+ from hypeline_core.models.alert_result import AlertResult
41
+ from hypeline_core.models.alert_spec import AlertSpec
42
+ from hypeline_core.models.content_diff import ContentDiff
43
+ from hypeline_core.models.copy import Copy
44
+ from hypeline_core.models.create_source_request import CreateSourceRequest
45
+ from hypeline_core.models.delivery import Delivery
46
+ from hypeline_core.models.delta_op import DeltaOp
47
+ from hypeline_core.models.delta_stats import DeltaStats
48
+ from hypeline_core.models.detect_dto import DetectDTO
49
+ from hypeline_core.models.detect_source_request import DetectSourceRequest
50
+ from hypeline_core.models.diff_segment import DiffSegment
51
+ from hypeline_core.models.error_detail import ErrorDetail
52
+ from hypeline_core.models.error_model import ErrorModel
53
+ from hypeline_core.models.event_kind_dto import EventKindDTO
54
+ from hypeline_core.models.health_dto import HealthDTO
55
+ from hypeline_core.models.history_page import HistoryPage
56
+ from hypeline_core.models.inclusion import Inclusion
57
+ from hypeline_core.models.instantiate_adhoc_template_request import InstantiateAdhocTemplateRequest
58
+ from hypeline_core.models.instantiate_template_request import InstantiateTemplateRequest
59
+ from hypeline_core.models.manifest import Manifest
60
+ from hypeline_core.models.next_steps import NextSteps
61
+ from hypeline_core.models.pdf_page_span import PDFPageSpan
62
+ from hypeline_core.models.parameter import Parameter
63
+ from hypeline_core.models.picker_box import PickerBox
64
+ from hypeline_core.models.picker_dto import PickerDTO
65
+ from hypeline_core.models.preview_row_dto import PreviewRowDTO
66
+ from hypeline_core.models.preview_template_request import PreviewTemplateRequest
67
+ from hypeline_core.models.receipt import Receipt
68
+ from hypeline_core.models.render_doc_body import RenderDocBody
69
+ from hypeline_core.models.render_doc_viewport import RenderDocViewport
70
+ from hypeline_core.models.render_doc_wait import RenderDocWait
71
+ from hypeline_core.models.render_multi_response import RenderMultiResponse
72
+ from hypeline_core.models.report import Report
73
+ from hypeline_core.models.signed_tree_head import SignedTreeHead
74
+ from hypeline_core.models.source_dto import SourceDTO
75
+ from hypeline_core.models.source_leaf_entry import SourceLeafEntry
76
+ from hypeline_core.models.source_log_page import SourceLogPage
77
+ from hypeline_core.models.source_result import SourceResult
78
+ from hypeline_core.models.source_spec import SourceSpec
79
+ from hypeline_core.models.stream_event import StreamEvent
80
+ from hypeline_core.models.summary import Summary
81
+ from hypeline_core.models.template_summary_dto import TemplateSummaryDTO
82
+ from hypeline_core.models.text_delta import TextDelta
83
+ from hypeline_core.models.typed_payload import TypedPayload
84
+ from hypeline_core.models.update_source_request import UpdateSourceRequest