raindrop-ai 0.0.19__py3-none-any.whl → 0.0.22__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/analytics.py CHANGED
@@ -1,14 +1,19 @@
1
1
  import sys
2
2
  import time
3
3
  import threading
4
- from typing import Union, List, Dict, Optional, Literal
4
+ from typing import Union, List, Dict, Optional, Literal, Any
5
5
  import requests
6
6
  from datetime import datetime, timezone
7
7
  import logging
8
8
  import json
9
9
  import uuid
10
10
  import atexit
11
+ from pydantic import ValidationError
12
+ from threading import Timer
11
13
  from raindrop.version import VERSION
14
+ from raindrop.models import TrackAIEvent, Attachment, SignalEvent, DefaultSignal, FeedbackSignal, EditSignal, PartialTrackAIEvent, PartialAIData
15
+ from raindrop.interaction import Interaction
16
+
12
17
 
13
18
 
14
19
  # Configure logging
@@ -27,6 +32,10 @@ flush_thread = None
27
32
  shutdown_event = threading.Event()
28
33
  max_ingest_size_bytes = 1 * 1024 * 1024 # 1 MB
29
34
 
35
+ _partial_buffers: dict[str, PartialTrackAIEvent] = {}
36
+ _partial_timers: dict[str, Timer] = {}
37
+ _PARTIAL_TIMEOUT = 2 # 2 seconds
38
+
30
39
  def set_debug_logs(value: bool):
31
40
  global debug_logs
32
41
  debug_logs = value
@@ -125,58 +134,49 @@ def identify(user_id: str, traits: Dict[str, Union[str, int, bool, float]]) -> N
125
134
  data = {"user_id": user_id, "traits": traits}
126
135
  save_to_buffer({"type": "users/identify", "data": data})
127
136
 
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
137
 
148
138
  def track_ai(
149
139
  user_id: str,
150
140
  event: str,
141
+ event_id: Optional[str] = None,
151
142
  model: Optional[str] = None,
152
- user_input: Optional[str] = None,
143
+ input: Optional[str] = None,
153
144
  output: Optional[str] = None,
154
145
  convo_id: Optional[str] = None,
155
146
  properties: Optional[Dict[str, Union[str, int, bool, float]]] = None,
156
147
  timestamp: Optional[str] = None,
157
- ) -> None:
148
+ attachments: Optional[List[Attachment]] = None,
149
+ ) -> str:
158
150
  if not _check_write_key():
159
151
  return
152
+
153
+ event_id = event_id or str(uuid.uuid4())
160
154
 
161
- if not user_input and not output:
162
- raise ValueError("One of user_input or output must be provided and not empty.")
155
+ try:
156
+ payload = TrackAIEvent(
157
+ event_id=event_id,
158
+ user_id=user_id,
159
+ event=event,
160
+ timestamp=timestamp or _get_timestamp(),
161
+ properties=properties or {},
162
+ ai_data=dict( # Pydantic will coerce to AIData
163
+ model=model,
164
+ input=input,
165
+ output=output,
166
+ convo_id=convo_id,
167
+ ),
168
+ attachments=attachments,
169
+ )
170
+ except ValidationError as err:
171
+ logger.error(f"[raindrop] Invalid data passed to track_ai: {err}")
172
+ return None
163
173
 
164
- event_id = str(uuid.uuid4())
165
174
 
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()
175
+ if payload.properties is None:
176
+ payload.properties = {}
177
+ payload.properties["$context"] = _get_context()
178
+
179
+ data = payload.model_dump(mode="json")
180
180
 
181
181
  size = _get_size(data)
182
182
  if size > max_ingest_size_bytes:
@@ -191,6 +191,9 @@ def track_ai(
191
191
 
192
192
  def shutdown():
193
193
  logger.info("Shutting down raindrop analytics")
194
+ for eid in list(_partial_timers.keys()):
195
+ _flush_partial_event(eid)
196
+
194
197
  shutdown_event.set()
195
198
  if flush_thread:
196
199
  flush_thread.join(timeout=10)
@@ -219,24 +222,25 @@ def _get_timestamp():
219
222
 
220
223
  def _get_size(event: dict[str, any]) -> int:
221
224
  try:
222
- data = json.dumps(event)
225
+ # Add default=str to handle types like datetime
226
+ data = json.dumps(event, default=str)
223
227
  return len(data.encode('utf-8'))
224
228
  except (TypeError, OverflowError) as e:
225
229
  logger.error(f"Error serializing event for size calculation: {e}")
226
230
  return 0
227
231
 
228
- # Signal types
229
- SignalType = Literal["default", "feedback", "edit"]
232
+ # Signal types - This is now defined in models.py
233
+ # SignalType = Literal["default", "feedback", "edit"]
230
234
 
231
235
  def track_signal(
232
236
  event_id: str,
233
237
  name: str,
234
- signal_type: Optional[SignalType] = "default",
238
+ signal_type: Literal["default", "feedback", "edit"] = "default",
235
239
  timestamp: Optional[str] = None,
236
- properties: Optional[Dict[str, any]] = None,
240
+ properties: Optional[Dict[str, Any]] = None,
237
241
  attachment_id: Optional[str] = None,
238
242
  comment: Optional[str] = None,
239
- after: Optional[str] = None,
243
+ after: Optional[str] = None
240
244
  ) -> None:
241
245
  """
242
246
  Track a signal event.
@@ -245,31 +249,55 @@ def track_signal(
245
249
  event_id: The ID of the event to attach the signal to
246
250
  name: Name of the signal (e.g. "thumbs_up", "thumbs_down")
247
251
  signal_type: Type of signal ("default", "feedback", or "edit")
248
- timestamp: Optional timestamp for the signal
249
- properties: Optional properties for the signal
252
+ timestamp: Optional timestamp for the signal (ISO 8601 format)
253
+ properties: Optional dictionary of additional properties.
250
254
  attachment_id: Optional ID of an attachment
251
- comment: Optional comment (only for feedback signals)
252
- after: Optional after content (only for edit signals)
255
+ comment: Optional comment string (required and used only if signal_type is 'feedback').
256
+ after: Optional after content string (required and used only if signal_type is 'edit').
253
257
  """
254
258
  if not _check_write_key():
255
259
  return
256
260
 
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 = {
261
+ # Prepare the final properties dictionary
262
+ final_properties = properties.copy() if properties else {}
263
+ if signal_type == "feedback" and comment is not None:
264
+ if "comment" in final_properties:
265
+ logger.warning("'comment' provided as both argument and in properties; argument value used.")
266
+ final_properties["comment"] = comment
267
+ elif signal_type == "edit" and after is not None:
268
+ if "after" in final_properties:
269
+ logger.warning("'after' provided as both argument and in properties; argument value used.")
270
+ final_properties["after"] = after
271
+
272
+ # Prepare base arguments for all signal types
273
+ base_args = {
265
274
  "event_id": event_id,
266
275
  "signal_name": name,
267
- "signal_type": signal_type,
268
- "timestamp": timestamp if timestamp else _get_timestamp(),
269
- "properties": signal_properties,
276
+ "timestamp": timestamp or _get_timestamp(),
277
+ "properties": final_properties,
270
278
  "attachment_id": attachment_id,
271
279
  }
272
280
 
281
+ try:
282
+ # Construct the specific signal model based on signal_type
283
+ if signal_type == "feedback":
284
+ payload = FeedbackSignal(**base_args, signal_type=signal_type)
285
+ elif signal_type == "edit":
286
+ payload = EditSignal(**base_args, signal_type=signal_type)
287
+ else: # signal_type == "default"
288
+ if comment is not None:
289
+ logger.warning("'comment' argument provided for non-feedback signal type; ignored.")
290
+ if after is not None:
291
+ logger.warning("'after' argument provided for non-edit signal type; ignored.")
292
+ payload = DefaultSignal(**base_args, signal_type=signal_type)
293
+
294
+ except ValidationError as err:
295
+ logger.error(f"[raindrop] Invalid data passed to track_signal: {err}")
296
+ return None
297
+
298
+ # model_dump handles the timestamp correctly
299
+ data = payload.model_dump(mode='json')
300
+
273
301
  size = _get_size(data)
274
302
  if size > max_ingest_size_bytes:
275
303
  logger.warning(
@@ -280,4 +308,99 @@ def track_signal(
280
308
 
281
309
  save_to_buffer({"type": "signals/track", "data": data})
282
310
 
311
+ def begin(
312
+ user_id: str,
313
+ event: str,
314
+ event_id: str | None = None,
315
+ properties: Optional[Dict[str, Any]] = None,
316
+ input: Optional[str] = None,
317
+ attachments: Optional[List[Attachment]] = None,
318
+ convo_id: Optional[str] = None,
319
+ ) -> Interaction:
320
+ """
321
+ Starts (or resumes) an interaction and returns a helper object.
322
+ """
323
+ eid = event_id or str(uuid.uuid4())
324
+
325
+ # Prepare ai_data if input is provided
326
+ ai_data_partial = None
327
+ if input:
328
+ ai_data_partial = PartialAIData(input=input, convo_id=convo_id)
329
+
330
+ # Combine properties with initial_fields, giving precedence to initial_fields if keys clash
331
+ final_properties = (properties or {}).copy()
332
+
333
+ partial_event = PartialTrackAIEvent(
334
+ event_id=eid,
335
+ user_id=user_id,
336
+ event=event,
337
+ ai_data=ai_data_partial,
338
+ properties=final_properties or None, # Pass None if empty, matching PartialTrackAIEvent defaults
339
+ attachments=attachments,
340
+ )
341
+
342
+ _track_ai_partial(partial_event)
343
+ return Interaction(eid)
344
+
345
+ def resume_interaction(event_id: str) -> Interaction:
346
+ """Return an Interaction wrapper for an existing eventId."""
347
+ return Interaction(event_id)
348
+
349
+ def _track_ai_partial(event: PartialTrackAIEvent) -> None:
350
+ """
351
+ Merge the incoming patch into an in-memory doc and flush to backend:
352
+ • on `.finish()` (is_pending == False)
353
+ • or after 20 s of inactivity
354
+ """
355
+ eid = event.event_id
356
+
357
+ # 1. merge
358
+ existing = _partial_buffers.get(eid, PartialTrackAIEvent(event_id=eid))
359
+ existing.is_pending = existing.is_pending if existing.is_pending is not None else True
360
+ merged_dict = existing.model_dump(exclude_none=True)
361
+ incoming = event.model_dump(exclude_none=True)
362
+
363
+ # deep merge ai_data / properties
364
+ def _deep(d: dict, u: dict):
365
+ for k, v in u.items():
366
+ d[k] = _deep(d.get(k, {}) if isinstance(v, dict) else v, v) if isinstance(v, dict) else v
367
+ return d
368
+ merged = _deep(merged_dict, incoming)
369
+ merged_obj = PartialTrackAIEvent(**merged)
370
+
371
+ _partial_buffers[eid] = merged_obj
372
+
373
+ # 2. timer handling
374
+ if t := _partial_timers.get(eid):
375
+ t.cancel()
376
+ if merged_obj.is_pending is False:
377
+ _flush_partial_event(eid)
378
+ else:
379
+ _partial_timers[eid] = Timer(_PARTIAL_TIMEOUT, _flush_partial_event, args=[eid])
380
+ _partial_timers[eid].daemon = True
381
+ _partial_timers[eid].start()
382
+
383
+ if debug_logs:
384
+ logger.debug(f"[raindrop] updated partial {eid}: {merged_obj.model_dump(exclude_none=True)}")
385
+
386
+ def _flush_partial_event(event_id: str) -> None:
387
+ """
388
+ Send the accumulated patch as a single object to `events/track_partial`.
389
+ """
390
+ if t := _partial_timers.pop(event_id, None):
391
+ t.cancel()
392
+
393
+ evt = _partial_buffers.pop(event_id, None)
394
+ if not evt:
395
+ return
396
+
397
+ # convert to ordinary TrackAIEvent-ish dict before send
398
+ data = evt.model_dump(mode="json", exclude_none=True)
399
+ size = _get_size(data)
400
+ if size > max_ingest_size_bytes:
401
+ logger.warning(f"[raindrop] partial event {event_id} > 1 MB; skipping")
402
+ return
403
+
404
+ send_request("events/track_partial", data)
405
+
283
406
  atexit.register(shutdown)
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Dict, List, Optional, Callable
3
+ from uuid import uuid4
4
+
5
+ from .models import Attachment, PartialTrackAIEvent
6
+ from . import analytics as _core
7
+
8
+
9
+ class Interaction:
10
+ """
11
+ Thin helper returned by analytics.begin().
12
+ Each mutator just relays a partial update back to Analytics.
13
+ """
14
+ __slots__ = ("_event_id", "_analytics")
15
+
16
+ def __init__(self, event_id: Optional[str] = None):
17
+ self._event_id = event_id or str(uuid4())
18
+ self._analytics = _core
19
+
20
+
21
+ # -- mutators ----------------------------------------------------------- #
22
+ def set_input(self, text: str) -> None:
23
+ self._analytics._track_ai_partial(
24
+ PartialTrackAIEvent(event_id=self._event_id, ai_data={"input": text})
25
+ )
26
+
27
+ def add_attachments(self, attachments: List[Attachment]) -> None:
28
+ self._analytics._track_ai_partial(
29
+ PartialTrackAIEvent(event_id=self._event_id, attachments=attachments)
30
+ )
31
+
32
+ def set_properties(self, props: Dict[str, Any]) -> None:
33
+ self._analytics._track_ai_partial(
34
+ PartialTrackAIEvent(event_id=self._event_id, properties=props)
35
+ )
36
+
37
+ def finish(self, *, output: str | None = None, **extra) -> None:
38
+ payload = PartialTrackAIEvent(
39
+ event_id=self._event_id,
40
+ ai_data={"output": output} if output is not None else None,
41
+ is_pending=False,
42
+ **extra,
43
+ )
44
+ self._analytics._track_ai_partial(payload)
45
+
46
+ # convenience
47
+ @property
48
+ def id(self) -> str:
49
+ return self._event_id
raindrop/models.py ADDED
@@ -0,0 +1,133 @@
1
+ from pydantic import BaseModel, Field, ValidationError, model_validator, field_validator
2
+ from typing import Any, Optional, Dict, Literal, List, Union
3
+ from datetime import datetime, timezone
4
+
5
+ class _Base(BaseModel):
6
+ model_config = dict(extra="forbid", validate_default=True)
7
+
8
+
9
+ class Attachment(BaseModel):
10
+ type: Literal["code", "text", "image", "iframe"]
11
+ value: str # URL, raw code, etc.
12
+ name: Optional[str] = None # e.g. "Generated SQL"
13
+ role: Optional[Literal["input", "output", "context"]] = None
14
+ language: Optional[str] = None # for code snippets
15
+
16
+ @model_validator(mode="after")
17
+ def _require_value(cls, values):
18
+ if not values.value:
19
+ raise ValueError("value must be non-empty.")
20
+ return values
21
+
22
+ class AIData(_Base):
23
+ model: Optional[str]
24
+ input: Optional[str]
25
+ output: Optional[str]
26
+ convo_id: Optional[str]
27
+
28
+ @model_validator(mode="after")
29
+ def _require_input_or_output(cls, values):
30
+ if not (values.input or values.output):
31
+ raise ValueError("Either 'input' or 'output' must be non-empty.")
32
+ return values
33
+
34
+ class TrackAIEvent(_Base):
35
+ event_id: Optional[str] = None
36
+ user_id: str
37
+ event: str
38
+ ai_data: AIData
39
+ properties: Dict[str, Any] = Field(default_factory=dict)
40
+ timestamp: datetime = Field(
41
+ default_factory=lambda: datetime.now(timezone.utc).replace(microsecond=0).isoformat()
42
+ )
43
+ attachments: Optional[List[Attachment]] = None
44
+
45
+ # Ensure user_id and event are non-empty strings
46
+ @field_validator("user_id", "event")
47
+ def _non_empty(cls, v, info):
48
+ if v is None or (isinstance(v, str) and v.strip() == ""):
49
+ raise ValueError(f"'{info.field_name}' must be a non-empty string.")
50
+ return v
51
+
52
+ # No need to duplicate input/output check here; AIData already enforces it
53
+ # but keep method to return values unchanged so that pydantic doesn't complain about unused return
54
+
55
+
56
+ # --- Signal Tracking Models --- #
57
+
58
+ class BaseSignal(_Base):
59
+ """Base model for signal events, containing common fields."""
60
+ event_id: str
61
+ signal_name: str
62
+ timestamp: datetime = Field(
63
+ # Return a datetime object; Pydantic's model_dump will handle serialization to string
64
+ default_factory=lambda: datetime.now(timezone.utc).replace(microsecond=0)
65
+ )
66
+ properties: Dict[str, Any] = Field(default_factory=dict)
67
+ attachment_id: Optional[str] = None
68
+
69
+ @field_validator("event_id", "signal_name")
70
+ def _non_empty_strings(cls, v, info):
71
+ if not isinstance(v, str) or not v.strip():
72
+ raise ValueError(f"'{info.field_name}' must be a non-empty string.")
73
+ return v
74
+
75
+ class DefaultSignal(BaseSignal):
76
+ """Model for default signal events."""
77
+ signal_type: Literal["default"] = "default"
78
+
79
+ class FeedbackSignal(BaseSignal):
80
+ """Model for feedback signal events, requiring a comment."""
81
+ signal_type: Literal["feedback"]
82
+
83
+ @model_validator(mode="after")
84
+ def _check_comment_in_properties(cls, values):
85
+ # Check properties safely after potential initialization
86
+ # Use getattr to safely access properties, returning None if not present
87
+ props = getattr(values, 'properties', None)
88
+ if not isinstance(props, dict):
89
+ raise ValueError("'properties' must be a dictionary for feedback signals.")
90
+ comment = props.get("comment")
91
+ if not comment or not isinstance(comment, str) or not comment.strip():
92
+ raise ValueError("'properties' must contain a non-empty string 'comment' for feedback signals.")
93
+ return values
94
+
95
+ class EditSignal(BaseSignal):
96
+ """Model for edit signal events, requiring after content."""
97
+ signal_type: Literal["edit"]
98
+
99
+ @model_validator(mode="after")
100
+ def _check_after_in_properties(cls, values):
101
+ # Check properties safely after potential initialization
102
+ props = getattr(values, 'properties', None)
103
+ if not isinstance(props, dict):
104
+ raise ValueError("'properties' must be a dictionary for edit signals.")
105
+ after = props.get("after")
106
+ if not after or not isinstance(after, str) or not after.strip():
107
+ raise ValueError("'properties' must contain a non-empty string 'after' for edit signals.")
108
+ return values
109
+
110
+ # Discriminated Union for Signal Events
111
+ # Pydantic will automatically use the 'signal_type' field to determine which model to use.
112
+ SignalEvent = Union[DefaultSignal, FeedbackSignal, EditSignal]
113
+
114
+ # --- End Signal Tracking Models --- #
115
+
116
+
117
+ class PartialAIData(_Base):
118
+ """Looser version for incremental updates."""
119
+ model: Optional[str] = None
120
+ input: Optional[str] = None
121
+ output: Optional[str] = None
122
+ convo_id: Optional[str] = None
123
+
124
+ class PartialTrackAIEvent(_Base):
125
+ """Accepts *any subset* of TrackAIEvent fields."""
126
+ event_id: str # always required for merge-key
127
+ user_id: Optional[str] = None
128
+ event: Optional[str] = None
129
+ ai_data: Optional[PartialAIData] = None
130
+ timestamp: Optional[datetime] = None
131
+ properties: Optional[Dict[str, Any]] = None
132
+ attachments: Optional[List[Attachment]] = None
133
+ is_pending: Optional[bool] = True
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.1
2
+ Name: raindrop-ai
3
+ Version: 0.0.22
4
+ Summary: Raindrop AI (Python SDK)
5
+ License: MIT
6
+ Author: Raindrop AI
7
+ Author-email: sdk@raindrop.ai
8
+ Requires-Python: >=3.11, !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*, !=3.8.*, !=3.9.*, !=3.10.*
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Dist: pydantic (>=2.11,<3)
14
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Raindrop Python SDK
18
+
19
+ ## Installation dependencies
20
+
21
+
22
+ ```bash
23
+ pip install poetry
24
+ ```
25
+
26
+ ```bash
27
+ poetry install
28
+ ```
29
+
30
+
31
+ ## Run tests
32
+
33
+ ```bash
34
+ poetry run green -vv
35
+ ```
36
+
37
+
38
+
39
+
40
+
41
+
@@ -0,0 +1,8 @@
1
+ raindrop/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ raindrop/analytics.py,sha256=N-thdcRHcHRN_-ywFW28X8LmIjpoG0VW0WEmtKDJZYQ,13477
3
+ raindrop/interaction.py,sha256=U8OapN5tZGSMVkn-dTLum5sIg_AgSYKX9lrtwgXJOuI,1613
4
+ raindrop/models.py,sha256=THs3VQXGfJAdfg99rEBlqUzDh_F1HBC2hMOQx49l6BQ,5229
5
+ raindrop/version.py,sha256=zKIDJIJluqVEGWC_VXIxFo3Hrk5Q7UGBBqP5uigDAN8,18
6
+ raindrop_ai-0.0.22.dist-info/METADATA,sha256=w20nYZv42yzfYrPkWAXVhmq_YaxywHi7ymJzIiezOkU,774
7
+ raindrop_ai-0.0.22.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
8
+ raindrop_ai-0.0.22.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.1)
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1,28 +0,0 @@
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
@@ -1,7 +0,0 @@
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,,
@@ -1 +0,0 @@
1
- raindrop