raindrop-ai 0.0.19__py3-none-any.whl

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.
raindrop/__init__.py ADDED
File without changes
raindrop/analytics.py ADDED
@@ -0,0 +1,283 @@
1
+ import sys
2
+ import time
3
+ import threading
4
+ from typing import Union, List, Dict, Optional, Literal
5
+ import requests
6
+ from datetime import datetime, timezone
7
+ import logging
8
+ import json
9
+ import uuid
10
+ import atexit
11
+ from raindrop.version import VERSION
12
+
13
+
14
+ # Configure logging
15
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
16
+ logger = logging.getLogger(__name__)
17
+
18
+ write_key = None
19
+ api_url = "https://api.raindrop.ai/v1/"
20
+ max_queue_size = 10000
21
+ upload_size = 10
22
+ upload_interval = 1.0
23
+ buffer = []
24
+ flush_lock = threading.Lock()
25
+ debug_logs = False
26
+ flush_thread = None
27
+ shutdown_event = threading.Event()
28
+ max_ingest_size_bytes = 1 * 1024 * 1024 # 1 MB
29
+
30
+ def set_debug_logs(value: bool):
31
+ global debug_logs
32
+ debug_logs = value
33
+ if debug_logs:
34
+ logger.setLevel(logging.DEBUG)
35
+ else:
36
+ logger.setLevel(logging.INFO)
37
+
38
+ def start_flush_thread():
39
+ logger.debug("Opening flush thread")
40
+ global flush_thread
41
+ if flush_thread is None:
42
+ flush_thread = threading.Thread(target=flush_loop)
43
+ flush_thread.daemon = True
44
+ flush_thread.start()
45
+
46
+ def flush_loop():
47
+ while not shutdown_event.is_set():
48
+ try:
49
+ flush()
50
+ except Exception as e:
51
+ logger.error(f"Error in flush loop: {e}")
52
+ time.sleep(upload_interval)
53
+
54
+ def flush() -> None:
55
+ global buffer
56
+
57
+ if buffer is None:
58
+ logger.error("No buffer available")
59
+ return
60
+
61
+ logger.debug("Starting flush")
62
+
63
+ with flush_lock:
64
+ current_buffer = buffer
65
+ buffer = []
66
+
67
+ logger.debug(f"Flushing buffer size: {len(current_buffer)}")
68
+
69
+ grouped_events = {}
70
+ for event in current_buffer:
71
+ endpoint = event["type"]
72
+ data = event["data"]
73
+ if endpoint not in grouped_events:
74
+ grouped_events[endpoint] = []
75
+ grouped_events[endpoint].append(data)
76
+
77
+ for endpoint, events_data in grouped_events.items():
78
+ for i in range(0, len(events_data), upload_size):
79
+ batch = events_data[i:i+upload_size]
80
+ logger.debug(f"Sending {len(batch)} events to {endpoint}")
81
+ send_request(endpoint, batch)
82
+
83
+ logger.debug("Flush complete")
84
+
85
+ def send_request(endpoint: str, data_entries: List[Dict[str, Union[str, Dict]]]) -> None:
86
+
87
+ url = f"{api_url}{endpoint}"
88
+ headers = {
89
+ "Content-Type": "application/json",
90
+ "Authorization": f"Bearer {write_key}",
91
+ }
92
+
93
+ max_retries = 3
94
+ for attempt in range(max_retries):
95
+ try:
96
+ response = requests.post(url, json=data_entries, headers=headers)
97
+ response.raise_for_status()
98
+ logger.debug(f"Request successful: {response.status_code}")
99
+ break
100
+ except requests.exceptions.RequestException as e:
101
+ logger.error(f"Error sending request (attempt {attempt + 1}/{max_retries}): {e}")
102
+ if attempt == max_retries - 1:
103
+ logger.error(f"Failed to send request after {max_retries} attempts")
104
+
105
+ def save_to_buffer(event: Dict[str, Union[str, Dict]]) -> None:
106
+ global buffer
107
+
108
+ if len(buffer) >= max_queue_size * 0.8:
109
+ logger.warning(f"Buffer is at {len(buffer) / max_queue_size * 100:.2f}% capacity")
110
+
111
+ if len(buffer) >= max_queue_size:
112
+ logger.error("Buffer is full. Discarding event.")
113
+ return
114
+
115
+ logger.debug(f"Adding event to buffer: {event}")
116
+
117
+ with flush_lock:
118
+ buffer.append(event)
119
+
120
+ start_flush_thread()
121
+
122
+ def identify(user_id: str, traits: Dict[str, Union[str, int, bool, float]]) -> None:
123
+ if not _check_write_key():
124
+ return
125
+ data = {"user_id": user_id, "traits": traits}
126
+ save_to_buffer({"type": "users/identify", "data": data})
127
+
128
+ def track(
129
+ user_id: str,
130
+ event: str,
131
+ properties: Optional[Dict[str, Union[str, int, bool, float]]] = None,
132
+ timestamp: Optional[str] = None,
133
+ ) -> None:
134
+ if not _check_write_key():
135
+ return
136
+
137
+ data = {
138
+ "event_id": str(uuid.uuid4()),
139
+ "user_id": user_id,
140
+ "event": event,
141
+ "properties": properties,
142
+ "timestamp": timestamp if timestamp else _get_timestamp(),
143
+ }
144
+ data.setdefault("properties", {})["$context"] = _get_context()
145
+
146
+ save_to_buffer({"type": "events/track", "data": data})
147
+
148
+ def track_ai(
149
+ user_id: str,
150
+ event: str,
151
+ model: Optional[str] = None,
152
+ user_input: Optional[str] = None,
153
+ output: Optional[str] = None,
154
+ convo_id: Optional[str] = None,
155
+ properties: Optional[Dict[str, Union[str, int, bool, float]]] = None,
156
+ timestamp: Optional[str] = None,
157
+ ) -> None:
158
+ if not _check_write_key():
159
+ return
160
+
161
+ if not user_input and not output:
162
+ raise ValueError("One of user_input or output must be provided and not empty.")
163
+
164
+ event_id = str(uuid.uuid4())
165
+
166
+ data = {
167
+ "event_id": event_id,
168
+ "user_id": user_id,
169
+ "event": event,
170
+ "properties": properties or {},
171
+ "timestamp": timestamp if timestamp else _get_timestamp(),
172
+ "ai_data": {
173
+ "model": model,
174
+ "input": user_input,
175
+ "output": output,
176
+ "convo_id": convo_id,
177
+ },
178
+ }
179
+ data.setdefault("properties", {})["$context"] = _get_context()
180
+
181
+ size = _get_size(data)
182
+ if size > max_ingest_size_bytes:
183
+ logger.warning(
184
+ f"[raindrop] Events larger than {max_ingest_size_bytes / (1024 * 1024)} MB may have properties truncated - "
185
+ f"an event of size {size / (1024 * 1024):.2f} MB was logged"
186
+ )
187
+ return None # Skip adding oversized events to buffer
188
+
189
+ save_to_buffer({"type": "events/track", "data": data})
190
+ return event_id
191
+
192
+ def shutdown():
193
+ logger.info("Shutting down raindrop analytics")
194
+ shutdown_event.set()
195
+ if flush_thread:
196
+ flush_thread.join(timeout=10)
197
+ flush() # Final flush to ensure all events are sent
198
+
199
+ def _check_write_key():
200
+ if write_key is None:
201
+ logger.warning("write_key is not set. Please set it before using raindrop analytics.")
202
+ return False
203
+ return True
204
+
205
+ def _get_context():
206
+ return {
207
+ "library": {
208
+ "name": "python-sdk",
209
+ "version": VERSION,
210
+ },
211
+ "metadata": {
212
+ "pyVersion": f"v{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
213
+ },
214
+ }
215
+
216
+ def _get_timestamp():
217
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
218
+
219
+
220
+ def _get_size(event: dict[str, any]) -> int:
221
+ try:
222
+ data = json.dumps(event)
223
+ return len(data.encode('utf-8'))
224
+ except (TypeError, OverflowError) as e:
225
+ logger.error(f"Error serializing event for size calculation: {e}")
226
+ return 0
227
+
228
+ # Signal types
229
+ SignalType = Literal["default", "feedback", "edit"]
230
+
231
+ def track_signal(
232
+ event_id: str,
233
+ name: str,
234
+ signal_type: Optional[SignalType] = "default",
235
+ timestamp: Optional[str] = None,
236
+ properties: Optional[Dict[str, any]] = None,
237
+ attachment_id: Optional[str] = None,
238
+ comment: Optional[str] = None,
239
+ after: Optional[str] = None,
240
+ ) -> None:
241
+ """
242
+ Track a signal event.
243
+
244
+ Args:
245
+ event_id: The ID of the event to attach the signal to
246
+ name: Name of the signal (e.g. "thumbs_up", "thumbs_down")
247
+ signal_type: Type of signal ("default", "feedback", or "edit")
248
+ timestamp: Optional timestamp for the signal
249
+ properties: Optional properties for the signal
250
+ attachment_id: Optional ID of an attachment
251
+ comment: Optional comment (only for feedback signals)
252
+ after: Optional after content (only for edit signals)
253
+ """
254
+ if not _check_write_key():
255
+ return
256
+
257
+ # Prepare properties with optional comment and after fields
258
+ signal_properties = properties or {}
259
+ if comment is not None:
260
+ signal_properties["comment"] = comment
261
+ if after is not None:
262
+ signal_properties["after"] = after
263
+
264
+ data = {
265
+ "event_id": event_id,
266
+ "signal_name": name,
267
+ "signal_type": signal_type,
268
+ "timestamp": timestamp if timestamp else _get_timestamp(),
269
+ "properties": signal_properties,
270
+ "attachment_id": attachment_id,
271
+ }
272
+
273
+ size = _get_size(data)
274
+ if size > max_ingest_size_bytes:
275
+ logger.warning(
276
+ f"[raindrop] Events larger than {max_ingest_size_bytes / (1024 * 1024)} MB may have properties truncated - "
277
+ f"an event of size {size / (1024 * 1024):.2f} MB was logged"
278
+ )
279
+ return # Skip adding oversized events to buffer
280
+
281
+ save_to_buffer({"type": "signals/track", "data": data})
282
+
283
+ atexit.register(shutdown)
raindrop/version.py ADDED
@@ -0,0 +1 @@
1
+ VERSION = "0.0.19"
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: raindrop-ai
3
+ Version: 0.0.19
4
+ Summary: Raindrop AI (Python SDK)
5
+ Home-page: https://raindrop.ai
6
+ Author: Raindrop AI
7
+ Author-email: sdk@raindrop.ai
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.6
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: requests
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: requires-dist
26
+ Dynamic: summary
27
+
28
+ For questions, email us at sdk@raindrop.ai
@@ -0,0 +1,7 @@
1
+ raindrop/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ raindrop/analytics.py,sha256=zrhvoAM65KSFTc9VWwBTGKhfMtB0H_7J-QmApRXpOKc,8585
3
+ raindrop/version.py,sha256=zKIDJIJluqVEGWC_VXIxFo3Hrk5Q7UGBBqP5uigDAN8,18
4
+ raindrop_ai-0.0.19.dist-info/METADATA,sha256=CXtCww_mqKXivz4eIhbwymtWo2Y-XM28tSUzBoWSzp4,881
5
+ raindrop_ai-0.0.19.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
6
+ raindrop_ai-0.0.19.dist-info/top_level.txt,sha256=yyf4RASIufJ0tUhUJ1-X3R05s9BuOU6dXCmv08UVpo8,9
7
+ raindrop_ai-0.0.19.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (79.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ raindrop