databubble 0.2.0__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.
- databubble-0.2.0/PKG-INFO +80 -0
- databubble-0.2.0/README.md +50 -0
- databubble-0.2.0/databubble/__init__.py +33 -0
- databubble-0.2.0/databubble/client.py +204 -0
- databubble-0.2.0/databubble/exceptions.py +47 -0
- databubble-0.2.0/databubble/journeys.py +359 -0
- databubble-0.2.0/databubble/memory.py +212 -0
- databubble-0.2.0/databubble/models.py +244 -0
- databubble-0.2.0/databubble/skills.py +335 -0
- databubble-0.2.0/databubble.egg-info/PKG-INFO +80 -0
- databubble-0.2.0/databubble.egg-info/SOURCES.txt +17 -0
- databubble-0.2.0/databubble.egg-info/dependency_links.txt +1 -0
- databubble-0.2.0/databubble.egg-info/requires.txt +10 -0
- databubble-0.2.0/databubble.egg-info/top_level.txt +1 -0
- databubble-0.2.0/pyproject.toml +43 -0
- databubble-0.2.0/setup.cfg +4 -0
- databubble-0.2.0/tests/test_sdk.py +271 -0
- databubble-0.2.0/tests/test_sdk_journeys.py +283 -0
- databubble-0.2.0/tests/test_smoke_live.py +91 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: databubble
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Statistical Intelligence as a Service — rigorous analysis skills via API
|
|
5
|
+
Author: DataBubble AI
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://databubble.ai
|
|
8
|
+
Project-URL: Documentation, https://api.databubble.ai
|
|
9
|
+
Project-URL: Repository, https://github.com/sss828412/databubble-python
|
|
10
|
+
Project-URL: Issues, https://github.com/sss828412/databubble-python/issues
|
|
11
|
+
Keywords: statistics,data analysis,machine learning,api
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Provides-Extra: recommended
|
|
23
|
+
Requires-Dist: httpx>=0.24; extra == "recommended"
|
|
24
|
+
Requires-Dist: pandas>=1.5; extra == "recommended"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: httpx>=0.24; extra == "dev"
|
|
27
|
+
Requires-Dist: pandas>=1.5; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# DataBubble SDK
|
|
32
|
+
|
|
33
|
+
Statistical Intelligence as a Service.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install databubble httpx pandas
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from databubble import DataBubble
|
|
43
|
+
import pandas as pd
|
|
44
|
+
|
|
45
|
+
db = DataBubble(api_key="dbk_...")
|
|
46
|
+
|
|
47
|
+
# Univariate analysis
|
|
48
|
+
result = db.skills.univariate(df["price"])
|
|
49
|
+
print(result.summary)
|
|
50
|
+
print(result.warnings)
|
|
51
|
+
|
|
52
|
+
# Missing value profiling
|
|
53
|
+
result = db.skills.missing_values(df)
|
|
54
|
+
|
|
55
|
+
# Export session memory for next session
|
|
56
|
+
mem = db.memory.export(df, label="POS data June 2026")
|
|
57
|
+
mem.save("pos_memory.json")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Available skills
|
|
61
|
+
|
|
62
|
+
| Skill | Input | What it does |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `univariate` | Series or DataFrame + column= | Distribution analysis, skewness, bounded ordinal detection, MNAR flags |
|
|
65
|
+
| `outliers` | Series or DataFrame + column= | IQR + Z-score outlier detection |
|
|
66
|
+
| `missing_values` | DataFrame | MCAR/MAR/MNAR profiling, treatment recommendations |
|
|
67
|
+
| `leakage` | DataFrame + outcome= | Post-outcome timing detection, correlation proxy check |
|
|
68
|
+
| `bivariate` | DataFrame + x= + y= | Relationship analysis, linearity check |
|
|
69
|
+
| `correlation` | DataFrame + x= + y= | Pearson + Spearman, non-linearity flag |
|
|
70
|
+
|
|
71
|
+
## Tiers
|
|
72
|
+
|
|
73
|
+
| Tier | Price | Calls/month | Skills |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| Developer | Free | 500 | Core analysis skills |
|
|
76
|
+
| Pro | $49/month | 10,000 | All skills |
|
|
77
|
+
| Business | $299/month | 100,000 | All skills + journey endpoints |
|
|
78
|
+
| Enterprise | Custom | Unlimited | Everything |
|
|
79
|
+
|
|
80
|
+
Get a key at [databubble.ai](https://databubble.ai).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# DataBubble SDK
|
|
2
|
+
|
|
3
|
+
Statistical Intelligence as a Service.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install databubble httpx pandas
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from databubble import DataBubble
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
db = DataBubble(api_key="dbk_...")
|
|
16
|
+
|
|
17
|
+
# Univariate analysis
|
|
18
|
+
result = db.skills.univariate(df["price"])
|
|
19
|
+
print(result.summary)
|
|
20
|
+
print(result.warnings)
|
|
21
|
+
|
|
22
|
+
# Missing value profiling
|
|
23
|
+
result = db.skills.missing_values(df)
|
|
24
|
+
|
|
25
|
+
# Export session memory for next session
|
|
26
|
+
mem = db.memory.export(df, label="POS data June 2026")
|
|
27
|
+
mem.save("pos_memory.json")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Available skills
|
|
31
|
+
|
|
32
|
+
| Skill | Input | What it does |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `univariate` | Series or DataFrame + column= | Distribution analysis, skewness, bounded ordinal detection, MNAR flags |
|
|
35
|
+
| `outliers` | Series or DataFrame + column= | IQR + Z-score outlier detection |
|
|
36
|
+
| `missing_values` | DataFrame | MCAR/MAR/MNAR profiling, treatment recommendations |
|
|
37
|
+
| `leakage` | DataFrame + outcome= | Post-outcome timing detection, correlation proxy check |
|
|
38
|
+
| `bivariate` | DataFrame + x= + y= | Relationship analysis, linearity check |
|
|
39
|
+
| `correlation` | DataFrame + x= + y= | Pearson + Spearman, non-linearity flag |
|
|
40
|
+
|
|
41
|
+
## Tiers
|
|
42
|
+
|
|
43
|
+
| Tier | Price | Calls/month | Skills |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| Developer | Free | 500 | Core analysis skills |
|
|
46
|
+
| Pro | $49/month | 10,000 | All skills |
|
|
47
|
+
| Business | $299/month | 100,000 | All skills + journey endpoints |
|
|
48
|
+
| Enterprise | Custom | Unlimited | Everything |
|
|
49
|
+
|
|
50
|
+
Get a key at [databubble.ai](https://databubble.ai).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# databubble/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
DataBubble SDK — Statistical Intelligence as a Service.
|
|
4
|
+
|
|
5
|
+
Quick start:
|
|
6
|
+
from databubble import DataBubble
|
|
7
|
+
db = DataBubble(api_key="dbk_...")
|
|
8
|
+
result = db.skills.univariate(df["price"])
|
|
9
|
+
print(result.summary)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from databubble.client import DataBubble
|
|
13
|
+
from databubble.models import SkillResult, MemoryResult, ReconciliationResult, JourneyResult
|
|
14
|
+
from databubble.exceptions import (
|
|
15
|
+
DataBubbleError, AuthError, ForbiddenError,
|
|
16
|
+
RateLimitError, SkillError, ServerError, SDKUsageError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "0.2.0"
|
|
20
|
+
__all__ = [
|
|
21
|
+
"DataBubble",
|
|
22
|
+
"SkillResult",
|
|
23
|
+
"MemoryResult",
|
|
24
|
+
"ReconciliationResult",
|
|
25
|
+
"JourneyResult",
|
|
26
|
+
"DataBubbleError",
|
|
27
|
+
"AuthError",
|
|
28
|
+
"ForbiddenError",
|
|
29
|
+
"RateLimitError",
|
|
30
|
+
"SkillError",
|
|
31
|
+
"ServerError",
|
|
32
|
+
"SDKUsageError",
|
|
33
|
+
]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# databubble/client.py
|
|
2
|
+
"""
|
|
3
|
+
DataBubble — root client.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from databubble import DataBubble
|
|
7
|
+
db = DataBubble(api_key="dbk_...")
|
|
8
|
+
result = db.skills.univariate(df["price"])
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from databubble.exceptions import (
|
|
17
|
+
AuthError, ForbiddenError, RateLimitError,
|
|
18
|
+
SkillError, ServerError, DataBubbleError,
|
|
19
|
+
)
|
|
20
|
+
from databubble.skills import SkillsClient
|
|
21
|
+
from databubble.memory import MemoryClient
|
|
22
|
+
from databubble.journeys import JourneysClient
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
DEFAULT_BASE_URL = "https://api.databubble.ai"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _HTTPClient:
|
|
29
|
+
"""
|
|
30
|
+
Thin HTTP client. Uses httpx if available, falls back to urllib.
|
|
31
|
+
Handles auth header injection and error mapping.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, api_key: str, base_url: str, timeout: float):
|
|
35
|
+
self._api_key = api_key
|
|
36
|
+
self._base_url = base_url.rstrip("/")
|
|
37
|
+
self._timeout = timeout
|
|
38
|
+
self._session = None
|
|
39
|
+
self._init_session()
|
|
40
|
+
|
|
41
|
+
def _init_session(self):
|
|
42
|
+
try:
|
|
43
|
+
import httpx
|
|
44
|
+
self._session = httpx.Client(
|
|
45
|
+
base_url=self._base_url,
|
|
46
|
+
headers={"X-API-Key": self._api_key},
|
|
47
|
+
timeout=self._timeout,
|
|
48
|
+
)
|
|
49
|
+
self._backend = "httpx"
|
|
50
|
+
except ImportError:
|
|
51
|
+
# Fall back to urllib — no session, headers injected per-request
|
|
52
|
+
self._backend = "urllib"
|
|
53
|
+
|
|
54
|
+
def _raise_for_status(self, status_code: int, body: dict):
|
|
55
|
+
# Route-level errors wrap in {"detail": {"error": ...}};
|
|
56
|
+
# middleware errors use flat {"error": ...}. Extract from both (M-9).
|
|
57
|
+
detail = body.get("detail") or {}
|
|
58
|
+
msg = (
|
|
59
|
+
(detail.get("error") if isinstance(detail, dict) else None)
|
|
60
|
+
or body.get("error")
|
|
61
|
+
or f"HTTP {status_code}"
|
|
62
|
+
)
|
|
63
|
+
if status_code == 401:
|
|
64
|
+
raise AuthError(msg, status_code, body)
|
|
65
|
+
if status_code == 403:
|
|
66
|
+
raise ForbiddenError(msg, status_code, body)
|
|
67
|
+
if status_code == 429:
|
|
68
|
+
raise RateLimitError(msg, status_code, body)
|
|
69
|
+
if status_code in (400, 422): # 422 was falling through to generic DataBubbleError (D-4)
|
|
70
|
+
raise SkillError(msg, status_code, body)
|
|
71
|
+
if status_code >= 500:
|
|
72
|
+
raise ServerError(f"Server error ({status_code}): {msg}", status_code, body)
|
|
73
|
+
if status_code >= 400:
|
|
74
|
+
raise DataBubbleError(msg, status_code, body)
|
|
75
|
+
|
|
76
|
+
def post_json(self, path: str, payload: dict) -> dict:
|
|
77
|
+
"""POST with JSON body. Returns parsed response dict."""
|
|
78
|
+
url = f"{self._base_url}{path}"
|
|
79
|
+
|
|
80
|
+
if self._backend == "httpx":
|
|
81
|
+
response = self._session.post(path, json=payload)
|
|
82
|
+
body = response.json()
|
|
83
|
+
self._raise_for_status(response.status_code, body)
|
|
84
|
+
return body
|
|
85
|
+
else:
|
|
86
|
+
import urllib.request, urllib.error
|
|
87
|
+
data = json.dumps(payload).encode()
|
|
88
|
+
req = urllib.request.Request(
|
|
89
|
+
url, data=data,
|
|
90
|
+
headers={
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
"X-API-Key": self._api_key,
|
|
93
|
+
},
|
|
94
|
+
method="POST",
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
98
|
+
return json.loads(resp.read())
|
|
99
|
+
except urllib.error.HTTPError as e:
|
|
100
|
+
body = json.loads(e.read())
|
|
101
|
+
self._raise_for_status(e.code, body)
|
|
102
|
+
# _raise_for_status always raises for 4xx/5xx; this is a safety net
|
|
103
|
+
raise ServerError(f"Unexpected response ({e.code})", e.code, body)
|
|
104
|
+
|
|
105
|
+
def post_multipart(self, path: str, fields: dict, files) -> dict:
|
|
106
|
+
"""
|
|
107
|
+
POST with multipart form data. Returns parsed response dict.
|
|
108
|
+
|
|
109
|
+
files may be:
|
|
110
|
+
- dict: {field_name: (filename, data, content_type)} — single file per field
|
|
111
|
+
- list of (field_name, (filename, data, content_type)) — supports repeated field names
|
|
112
|
+
for list[UploadFile] parameters (M-5: memory_files needs repeated "memory_files" key)
|
|
113
|
+
"""
|
|
114
|
+
url = f"{self._base_url}{path}"
|
|
115
|
+
|
|
116
|
+
if self._backend == "httpx":
|
|
117
|
+
form_data = {k: v for k, v in fields.items() if v is not None}
|
|
118
|
+
# httpx accepts files as a list of (name, content) tuples for repeated keys
|
|
119
|
+
if isinstance(files, dict):
|
|
120
|
+
file_list = [(k, v) for k, v in files.items()]
|
|
121
|
+
else:
|
|
122
|
+
file_list = list(files)
|
|
123
|
+
response = self._session.post(path, data=form_data, files=file_list)
|
|
124
|
+
body = response.json()
|
|
125
|
+
self._raise_for_status(response.status_code, body)
|
|
126
|
+
return body
|
|
127
|
+
else:
|
|
128
|
+
raise DataBubbleError(
|
|
129
|
+
"Multipart upload requires httpx. "
|
|
130
|
+
"Install with: pip install httpx"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def close(self):
|
|
134
|
+
if self._backend == "httpx" and self._session:
|
|
135
|
+
self._session.close()
|
|
136
|
+
|
|
137
|
+
def __enter__(self):
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
def __exit__(self, *args):
|
|
141
|
+
self.close()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class DataBubble:
|
|
145
|
+
"""
|
|
146
|
+
DataBubble API client.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
api_key: Your API key (starts with dbk_). Get one at databubble.ai.
|
|
150
|
+
base_url: API base URL. Defaults to https://api.databubble.ai.
|
|
151
|
+
Override for local development: http://localhost:8000
|
|
152
|
+
timeout: Request timeout in seconds. Default 60.
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
from databubble import DataBubble
|
|
156
|
+
db = DataBubble(api_key="dbk_...")
|
|
157
|
+
|
|
158
|
+
# Single-column skill
|
|
159
|
+
result = db.skills.univariate(df["price"])
|
|
160
|
+
print(result.summary)
|
|
161
|
+
print(result.warnings)
|
|
162
|
+
|
|
163
|
+
# Whole-dataset skill
|
|
164
|
+
result = db.skills.missing_values(df)
|
|
165
|
+
|
|
166
|
+
# Memory workflow
|
|
167
|
+
mem = db.memory.export(df, label="POS data June 2026")
|
|
168
|
+
mem.save("pos_memory.json")
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
api_key: Optional[str] = None,
|
|
174
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
175
|
+
timeout: float = 60.0,
|
|
176
|
+
):
|
|
177
|
+
import os
|
|
178
|
+
resolved_key = api_key or os.environ.get("DATABUBBLE_API_KEY", "")
|
|
179
|
+
if not resolved_key:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
"API key required. Pass api_key= or set DATABUBBLE_API_KEY env var. "
|
|
182
|
+
"Get a key at databubble.ai."
|
|
183
|
+
)
|
|
184
|
+
if not resolved_key.startswith("dbk_"):
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"Invalid API key format. Keys start with 'dbk_'. Got: {resolved_key[:8]}..."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
self._http = _HTTPClient(resolved_key, base_url, timeout)
|
|
190
|
+
self.skills = SkillsClient(self._http)
|
|
191
|
+
self.memory = MemoryClient(self._http)
|
|
192
|
+
self.journeys = JourneysClient(self._http)
|
|
193
|
+
|
|
194
|
+
def close(self):
|
|
195
|
+
self._http.close()
|
|
196
|
+
|
|
197
|
+
def __enter__(self):
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def __exit__(self, *args):
|
|
201
|
+
self.close()
|
|
202
|
+
|
|
203
|
+
def __repr__(self):
|
|
204
|
+
return f"DataBubble(base_url='{self._http._base_url}')"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# databubble/exceptions.py
|
|
2
|
+
"""
|
|
3
|
+
Typed exceptions for the DataBubble SDK.
|
|
4
|
+
Every HTTP error from the API maps to a specific exception class
|
|
5
|
+
so callers can handle them explicitly without parsing status codes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DataBubbleError(Exception):
|
|
10
|
+
"""Base exception for all SDK errors."""
|
|
11
|
+
def __init__(self, message: str, status_code: int = 0, response_body: dict = None):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.response_body = response_body or {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthError(DataBubbleError):
|
|
18
|
+
"""Invalid or missing API key. HTTP 401."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ForbiddenError(DataBubbleError):
|
|
23
|
+
"""Skill not available on this tier. HTTP 403."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RateLimitError(DataBubbleError):
|
|
28
|
+
"""Monthly call limit reached. HTTP 429."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SkillError(DataBubbleError):
|
|
33
|
+
"""Skill executed but returned an error (bad input, halted, etc). HTTP 400."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ServerError(DataBubbleError):
|
|
38
|
+
"""Unexpected server error. HTTP 5xx."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SDKUsageError(Exception):
|
|
43
|
+
"""
|
|
44
|
+
Raised for incorrect SDK usage — wrong argument types, missing required args.
|
|
45
|
+
Not an API error — never reaches the server.
|
|
46
|
+
"""
|
|
47
|
+
pass
|