pysolvr-client 0.1.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.
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: pysolvr-client
3
+ Version: 0.1.0
4
+ Summary: Shared notebook client library for pysolvr businesses
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/dAIvdMercer/pysolvr-client
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: requests>=2.28
10
+
11
+ # pysolvr-client
12
+
13
+ Shared notebook client library for [pysolvr](https://pysolvr.com) businesses.
14
+
15
+ Provides API wrapper, HTML display components, and Google Drive integration for Colab notebooks.
16
+
17
+ ## Install
18
+
19
+ ```
20
+ pip install pysolvr-client
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from pysolvr_client import ApiClient, Display, DriveManager
27
+
28
+ client = ApiClient(base_url, api_key)
29
+ ui = Display('#6366F1', '#10B981')
30
+ drive_mgr = DriveManager('my-business', ['outputs', 'reports'])
31
+ ```
@@ -0,0 +1,21 @@
1
+ # pysolvr-client
2
+
3
+ Shared notebook client library for [pysolvr](https://pysolvr.com) businesses.
4
+
5
+ Provides API wrapper, HTML display components, and Google Drive integration for Colab notebooks.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ pip install pysolvr-client
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from pysolvr_client import ApiClient, Display, DriveManager
17
+
18
+ client = ApiClient(base_url, api_key)
19
+ ui = Display('#6366F1', '#10B981')
20
+ drive_mgr = DriveManager('my-business', ['outputs', 'reports'])
21
+ ```
@@ -0,0 +1,15 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pysolvr-client"
7
+ version = "0.1.0"
8
+ description = "Shared notebook client library for pysolvr businesses"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.9"
12
+ dependencies = ["requests>=2.28"]
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/dAIvdMercer/pysolvr-client"
@@ -0,0 +1,4 @@
1
+ """pysolvr_client - Shared notebook client library for all pysolvr businesses."""
2
+ from .api import ApiClient
3
+ from .display import Display
4
+ from .drive import DriveManager
@@ -0,0 +1,82 @@
1
+ """API wrapper with auth, retries, and styled error/result handling."""
2
+ import requests
3
+ import time
4
+ from IPython.display import display, HTML, clear_output
5
+
6
+
7
+ class ApiClient:
8
+ def __init__(self, base_url: str, api_key: str):
9
+ self.base_url = base_url.rstrip("/")
10
+ self.headers = {"X-Api-Key": api_key, "Content-Type": "application/json"}
11
+
12
+ def call(self, method: str, path: str, payload: dict = None, retries: int = 2) -> dict:
13
+ """Make an API call with retries. Returns {ok, data, error, status, latency_ms}."""
14
+ url = f"{self.base_url}/{path.lstrip('/')}"
15
+ for attempt in range(retries + 1):
16
+ try:
17
+ start = time.time()
18
+ r = requests.request(method, url, headers=self.headers, json=payload, timeout=30)
19
+ latency_ms = int((time.time() - start) * 1000)
20
+ if r.status_code in (200, 202):
21
+ return {"ok": True, "data": r.json(), "status": r.status_code, "latency_ms": latency_ms}
22
+ elif r.status_code == 429 and attempt < retries:
23
+ time.sleep(2 ** attempt)
24
+ continue
25
+ else:
26
+ body = r.json() if r.headers.get("content-type", "").startswith("application/json") else {"message": r.text}
27
+ return {"ok": False, "error": body.get("error", body.get("message", "Unknown error")), "status": r.status_code, "latency_ms": latency_ms}
28
+ except requests.exceptions.Timeout:
29
+ if attempt < retries:
30
+ continue
31
+ return {"ok": False, "error": "Request timed out. Try again.", "status": 0, "latency_ms": 0}
32
+ except Exception as e:
33
+ return {"ok": False, "error": str(e), "status": 0, "latency_ms": 0}
34
+
35
+ def run_async(self, endpoint: str, body: dict, timeout: int = 120, poll_interval: int = 2) -> dict:
36
+ """Submit async job, poll with spinner, return result.
37
+
38
+ Returns {ok, data, error, status, latency_ms} — same shape as call().
39
+ Pre-flight rejections (400/402/403/429) return instantly.
40
+ """
41
+ resp = self.call("POST", endpoint, payload=body)
42
+
43
+ # Pre-flight rejection — no job created, return error immediately
44
+ if not resp["ok"]:
45
+ return resp
46
+
47
+ job_id = resp["data"].get("job_id")
48
+ if not job_id:
49
+ # Synchronous response (endpoint didn't use async pattern)
50
+ return resp
51
+
52
+ # Poll with spinner
53
+ start = time.time()
54
+ display(HTML('<div style="padding:8px;color:#888;">Working...</div>'))
55
+ while (time.time() - start) < timeout:
56
+ time.sleep(poll_interval)
57
+ poll = self.call("GET", f"/jobs/{job_id}")
58
+
59
+ if not poll["ok"]:
60
+ clear_output(wait=True)
61
+ return poll
62
+
63
+ status = poll["data"].get("status")
64
+ if status == "complete":
65
+ clear_output(wait=True)
66
+ elapsed_ms = int((time.time() - start) * 1000)
67
+ return {"ok": True, "data": poll["data"]["result"], "status": 200, "latency_ms": elapsed_ms}
68
+ elif status == "failed":
69
+ clear_output(wait=True)
70
+ return {"ok": False, "error": poll["data"].get("error", "Job failed"), "status": 500, "latency_ms": 0}
71
+
72
+ clear_output(wait=True)
73
+ return {"ok": False, "error": f"Timeout after {timeout}s (job_id: {job_id})", "status": 0, "latency_ms": 0}
74
+
75
+ def health_check(self) -> bool:
76
+ """Check API connectivity."""
77
+ result = self.call("GET", "/health")
78
+ return result["ok"] and result.get("data", {}).get("status") == "ok"
79
+
80
+ def get_usage(self) -> dict:
81
+ """Get usage data."""
82
+ return self.call("GET", "/usage")
@@ -0,0 +1,155 @@
1
+ """HTML rendering for notebook outputs - cards, tables, tabs, progress, errors."""
2
+ from IPython.display import HTML, display, clear_output
3
+
4
+
5
+ class Display:
6
+ def __init__(self, primary_color: str = "#6366F1", accent_color: str = "#10B981"):
7
+ self.primary = primary_color
8
+ self.accent = accent_color
9
+ self._styles = f"""
10
+ <style>
11
+ .pysolvr-card {{ background: #334155; border-radius: 8px; padding: 20px; margin: 8px 0; border: 1px solid #475569; font-family: Inter, system-ui, sans-serif; color: #f1f5f9; }}
12
+ .pysolvr-card h3 {{ margin: 0 0 12px 0; font-size: 16px; font-weight: 500; }}
13
+ .pysolvr-header {{ background: linear-gradient(135deg, {primary_color}22, {accent_color}22); border: 1px solid {primary_color}44; border-radius: 12px; padding: 24px; text-align: center; font-family: Inter, system-ui, sans-serif; color: #f1f5f9; }}
14
+ .pysolvr-header h1 {{ margin: 0; font-size: 28px; font-weight: 300; }}
15
+ .pysolvr-header p {{ margin: 8px 0 0; color: #94a3b8; font-size: 14px; }}
16
+ .pysolvr-badge {{ display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; }}
17
+ .pysolvr-success {{ background: #10b98122; color: #10b981; }}
18
+ .pysolvr-warning {{ background: #f59e0b22; color: #f59e0b; }}
19
+ .pysolvr-error {{ background: #ef444422; color: #ef4444; }}
20
+ .pysolvr-table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
21
+ .pysolvr-table th {{ text-align: left; padding: 8px 12px; border-bottom: 1px solid #475569; color: #94a3b8; font-weight: 500; }}
22
+ .pysolvr-table td {{ padding: 8px 12px; border-bottom: 1px solid #47556944; }}
23
+ .pysolvr-bar {{ height: 8px; border-radius: 4px; background: #1e293b; overflow: hidden; }}
24
+ .pysolvr-bar-fill {{ height: 100%; border-radius: 4px; transition: width 0.3s; }}
25
+ .pysolvr-tabs {{ display: flex; gap: 0; border-bottom: 1px solid #475569; margin-bottom: 16px; }}
26
+ .pysolvr-tab {{ padding: 8px 16px; cursor: pointer; color: #94a3b8; font-size: 13px; border-bottom: 2px solid transparent; }}
27
+ .pysolvr-tab.active {{ color: #f1f5f9; border-bottom-color: {primary_color}; }}
28
+ .pysolvr-tab-content {{ display: none; }}
29
+ .pysolvr-tab-content.active {{ display: block; }}
30
+ .pysolvr-cost {{ display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: #94a3b8; background: #1e293b; padding: 2px 8px; border-radius: 4px; }}
31
+ </style>
32
+ """
33
+
34
+ def header(self, name: str, tagline: str, version: str):
35
+ """Render business branding header."""
36
+ html = f"""{self._styles}
37
+ <div class="pysolvr-header">
38
+ <h1>{name}</h1>
39
+ <p>{tagline}</p>
40
+ <p style="margin-top:12px"><span class="pysolvr-badge" style="background:{self.primary}22;color:{self.primary}">v{version}</span></p>
41
+ </div>"""
42
+ display(HTML(html))
43
+
44
+ def card(self, title: str, content: str, status: str = None, actions: list = None):
45
+ """Render a styled result card."""
46
+ status_html = ""
47
+ if status == "success":
48
+ status_html = '<span class="pysolvr-badge pysolvr-success">Success</span>'
49
+ elif status == "warning":
50
+ status_html = '<span class="pysolvr-badge pysolvr-warning">Warning</span>'
51
+ elif status == "error":
52
+ status_html = '<span class="pysolvr-badge pysolvr-error">Error</span>'
53
+
54
+ actions_html = ""
55
+ if actions:
56
+ actions_html = '<div style="margin-top:12px;display:flex;gap:8px">' + "".join(
57
+ f'<span style="font-size:12px;color:{self.primary};cursor:pointer">{a}</span>' for a in actions
58
+ ) + "</div>"
59
+
60
+ html = f"""{self._styles}
61
+ <div class="pysolvr-card">
62
+ <h3>{title} {status_html}</h3>
63
+ <div style="font-size:14px;line-height:1.6">{content}</div>
64
+ {actions_html}
65
+ </div>"""
66
+ display(HTML(html))
67
+
68
+ def table(self, data: list, columns: list = None):
69
+ """Render a styled table from list of dicts."""
70
+ if not data:
71
+ self.card("No data", "Nothing to display yet.")
72
+ return
73
+ if not columns:
74
+ columns = list(data[0].keys())
75
+ header = "".join(f"<th>{c}</th>" for c in columns)
76
+ rows = "".join(
77
+ "<tr>" + "".join(f"<td>{row.get(c, '')}</td>" for c in columns) + "</tr>"
78
+ for row in data
79
+ )
80
+ html = f"""{self._styles}
81
+ <div class="pysolvr-card">
82
+ <table class="pysolvr-table"><thead><tr>{header}</tr></thead><tbody>{rows}</tbody></table>
83
+ </div>"""
84
+ display(HTML(html))
85
+
86
+ def progress(self, message: str = "Working..."):
87
+ """Show a progress spinner (call clear_output() then result when done)."""
88
+ html = f"""{self._styles}
89
+ <div class="pysolvr-card" style="text-align:center">
90
+ <div style="display:inline-block;width:20px;height:20px;border:2px solid #475569;border-top-color:{self.primary};border-radius:50%;animation:spin 0.8s linear infinite"></div>
91
+ <p style="margin:8px 0 0;color:#94a3b8;font-size:13px">{message}</p>
92
+ </div>
93
+ <style>@keyframes spin{{from{{transform:rotate(0deg)}}to{{transform:rotate(360deg)}}}}</style>"""
94
+ display(HTML(html))
95
+
96
+ def error(self, message: str, suggestion: str = None):
97
+ """Render a friendly error card."""
98
+ suggestion_html = f'<p style="margin-top:8px;font-size:12px;color:#94a3b8">Suggestion: {suggestion}</p>' if suggestion else ""
99
+ html = f"""{self._styles}
100
+ <div class="pysolvr-card" style="border-color:#ef444444">
101
+ <h3><span class="pysolvr-badge pysolvr-error">Error</span></h3>
102
+ <p style="font-size:14px">{message}</p>
103
+ {suggestion_html}
104
+ </div>"""
105
+ display(HTML(html))
106
+
107
+ def success(self, message: str, details: str = None):
108
+ """Render a success card."""
109
+ details_html = f'<p style="margin-top:8px;font-size:12px;color:#94a3b8">{details}</p>' if details else ""
110
+ html = f"""{self._styles}
111
+ <div class="pysolvr-card" style="border-color:#10b98144">
112
+ <h3><span class="pysolvr-badge pysolvr-success">Done</span></h3>
113
+ <p style="font-size:14px">{message}</p>
114
+ {details_html}
115
+ </div>"""
116
+ display(HTML(html))
117
+
118
+ def usage_bar(self, spent: float, limit: float, label: str = "Monthly usage"):
119
+ """Render a usage progress bar."""
120
+ pct = min((spent / limit) * 100, 100) if limit > 0 else 0
121
+ color = self.accent if pct < 80 else ("#f59e0b" if pct < 95 else "#ef4444")
122
+ html = f"""{self._styles}
123
+ <div class="pysolvr-card">
124
+ <h3>{label}</h3>
125
+ <div style="display:flex;justify-content:space-between;font-size:12px;color:#94a3b8;margin-bottom:6px">
126
+ <span>${spent:.2f} used</span><span>${limit:.2f} limit</span>
127
+ </div>
128
+ <div class="pysolvr-bar"><div class="pysolvr-bar-fill" style="width:{pct}%;background:{color}"></div></div>
129
+ <p style="margin-top:6px;font-size:11px;color:#94a3b8">${limit-spent:.2f} remaining ({100-pct:.0f}%)</p>
130
+ </div>"""
131
+ display(HTML(html))
132
+
133
+ def cost_badge(self, tokens: int, cost_usd: float):
134
+ """Render an inline cost badge."""
135
+ html = f'<span class="pysolvr-cost">{tokens:,} tokens | ${cost_usd:.4f}</span>'
136
+ display(HTML(self._styles + html))
137
+
138
+ def tabs(self, tab_dict: dict):
139
+ """Render tabbed content. tab_dict = {label: html_content}."""
140
+ import uuid
141
+ uid = uuid.uuid4().hex[:8]
142
+ tab_headers = ""
143
+ tab_bodies = ""
144
+ for i, (label, content) in enumerate(tab_dict.items()):
145
+ active = " active" if i == 0 else ""
146
+ tab_headers += f'<div class="pysolvr-tab{active}" onclick="document.querySelectorAll(\\'.pysolvr-tab-{uid}\\').forEach(e=>e.classList.remove(\\'active\\'));document.querySelectorAll(\\'.pysolvr-tc-{uid}\\').forEach(e=>e.classList.remove(\\'active\\'));this.classList.add(\\'active\\');document.getElementById(\\'tc-{uid}-{i}\\').classList.add(\\'active\\')">{label}</div>'
147
+ tab_bodies += f'<div id="tc-{uid}-{i}" class="pysolvr-tc-{uid} pysolvr-tab-content{active}">{content}</div>'
148
+ # Fix class on tabs for JS targeting
149
+ tab_headers = tab_headers.replace('class="pysolvr-tab', f'class="pysolvr-tab pysolvr-tab-{uid}')
150
+ html = f"""{self._styles}
151
+ <div class="pysolvr-card">
152
+ <div class="pysolvr-tabs">{tab_headers}</div>
153
+ {tab_bodies}
154
+ </div>"""
155
+ display(HTML(html))
@@ -0,0 +1,69 @@
1
+ """GDrive operations + .meta.json provenance tracking."""
2
+ import json
3
+ import os
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+
7
+
8
+ DRIVE_ROOT = "/content/drive/My Drive"
9
+
10
+
11
+ class DriveManager:
12
+ def __init__(self, slug: str, folders: list = None):
13
+ self.slug = slug
14
+ self.root = Path(DRIVE_ROOT) / "pysolvr" / slug
15
+ self.folders = folders or []
16
+
17
+ def ensure_folders(self):
18
+ """Create the pysolvr/{slug}/{folders} structure in GDrive."""
19
+ self.root.mkdir(parents=True, exist_ok=True)
20
+ for folder in self.folders:
21
+ (self.root / folder).mkdir(parents=True, exist_ok=True)
22
+ return str(self.root)
23
+
24
+ def save(self, folder: str, filename: str, content: str, meta: dict = None) -> str:
25
+ """Save a file + .meta.json companion. Returns the file path."""
26
+ path = self.root / folder / filename
27
+ path.parent.mkdir(parents=True, exist_ok=True)
28
+ path.write_text(content, encoding="utf-8")
29
+
30
+ if meta:
31
+ meta_path = path.with_suffix(path.suffix + ".meta.json")
32
+ meta["timestamp"] = datetime.now(tz=timezone.utc).isoformat()
33
+ meta["business_slug"] = self.slug
34
+ meta_path.write_text(json.dumps(meta, indent=2, default=str), encoding="utf-8")
35
+
36
+ return str(path)
37
+
38
+ def load(self, folder: str, filename: str) -> tuple:
39
+ """Load a file + its .meta.json. Returns (content, meta_dict)."""
40
+ path = self.root / folder / filename
41
+ if not path.exists():
42
+ return None, None
43
+ content = path.read_text(encoding="utf-8")
44
+ meta_path = path.with_suffix(path.suffix + ".meta.json")
45
+ meta = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {}
46
+ return content, meta
47
+
48
+ def list_files(self, folder: str) -> list:
49
+ """List files in a folder with metadata summaries."""
50
+ folder_path = self.root / folder
51
+ if not folder_path.exists():
52
+ return []
53
+ results = []
54
+ for f in sorted(folder_path.iterdir()):
55
+ if f.is_file() and not f.name.endswith(".meta.json"):
56
+ meta_path = f.with_suffix(f.suffix + ".meta.json")
57
+ meta = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {}
58
+ results.append({
59
+ "name": f.name,
60
+ "path": str(f),
61
+ "modified": datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
62
+ "meta": meta,
63
+ })
64
+ return results
65
+
66
+ def get_history(self, folder: str) -> list:
67
+ """Get provenance history from .meta.json files (for resumability)."""
68
+ files = self.list_files(folder)
69
+ return [{"file": f["name"], **f["meta"]} for f in files if f["meta"]]
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: pysolvr-client
3
+ Version: 0.1.0
4
+ Summary: Shared notebook client library for pysolvr businesses
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/dAIvdMercer/pysolvr-client
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: requests>=2.28
10
+
11
+ # pysolvr-client
12
+
13
+ Shared notebook client library for [pysolvr](https://pysolvr.com) businesses.
14
+
15
+ Provides API wrapper, HTML display components, and Google Drive integration for Colab notebooks.
16
+
17
+ ## Install
18
+
19
+ ```
20
+ pip install pysolvr-client
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from pysolvr_client import ApiClient, Display, DriveManager
27
+
28
+ client = ApiClient(base_url, api_key)
29
+ ui = Display('#6366F1', '#10B981')
30
+ drive_mgr = DriveManager('my-business', ['outputs', 'reports'])
31
+ ```
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ pysolvr_client/__init__.py
4
+ pysolvr_client/api.py
5
+ pysolvr_client/display.py
6
+ pysolvr_client/drive.py
7
+ pysolvr_client.egg-info/PKG-INFO
8
+ pysolvr_client.egg-info/SOURCES.txt
9
+ pysolvr_client.egg-info/dependency_links.txt
10
+ pysolvr_client.egg-info/requires.txt
11
+ pysolvr_client.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests>=2.28
@@ -0,0 +1 @@
1
+ pysolvr_client
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+