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.
- pysolvr_client-0.1.0/PKG-INFO +31 -0
- pysolvr_client-0.1.0/README.md +21 -0
- pysolvr_client-0.1.0/pyproject.toml +15 -0
- pysolvr_client-0.1.0/pysolvr_client/__init__.py +4 -0
- pysolvr_client-0.1.0/pysolvr_client/api.py +82 -0
- pysolvr_client-0.1.0/pysolvr_client/display.py +155 -0
- pysolvr_client-0.1.0/pysolvr_client/drive.py +69 -0
- pysolvr_client-0.1.0/pysolvr_client.egg-info/PKG-INFO +31 -0
- pysolvr_client-0.1.0/pysolvr_client.egg-info/SOURCES.txt +11 -0
- pysolvr_client-0.1.0/pysolvr_client.egg-info/dependency_links.txt +1 -0
- pysolvr_client-0.1.0/pysolvr_client.egg-info/requires.txt +1 -0
- pysolvr_client-0.1.0/pysolvr_client.egg-info/top_level.txt +1 -0
- pysolvr_client-0.1.0/setup.cfg +4 -0
|
@@ -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,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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pysolvr_client
|