picsha 0.1.1__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.
- picsha-0.1.1/PKG-INFO +73 -0
- picsha-0.1.1/README.md +54 -0
- picsha-0.1.1/picsha/__init__.py +16 -0
- picsha-0.1.1/picsha/client.py +127 -0
- picsha-0.1.1/picsha/exceptions.py +14 -0
- picsha-0.1.1/picsha/models.py +49 -0
- picsha-0.1.1/pyproject.toml +19 -0
picsha-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: picsha
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Official Python SDK for Picsha AI - Store, Encode, Understand.
|
|
5
|
+
Author: Peter
|
|
6
|
+
Author-email: peter@picsha.ai
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Dist: httpx (>=0.27.0,<0.28.0)
|
|
16
|
+
Requires-Dist: pydantic (>=2.6.0,<3.0.0)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Picsha AI Python SDK 🐍
|
|
20
|
+
|
|
21
|
+
The official Python client for Picsha AI. Designed specifically for data scientists, computational biologists, and ML engineers.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
We recommend using poetry:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
poetry add picsha
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or pip:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install picsha
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import picsha
|
|
41
|
+
|
|
42
|
+
client = picsha.Client(api_key="sk_your_key_here")
|
|
43
|
+
|
|
44
|
+
# 1. Semantic Search
|
|
45
|
+
results = client.search(
|
|
46
|
+
query="fluorescent cancer cell cultures",
|
|
47
|
+
mode="ai" # Hybrid Vector Search
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 2. Get transformed URLs for ML pipelines
|
|
51
|
+
for asset in results.assets:
|
|
52
|
+
url = asset.generate_url(width=512, height=512, format="webp")
|
|
53
|
+
print(url)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Asynchronous Support
|
|
57
|
+
|
|
58
|
+
For high-throughput workloads, an `AsyncClient` is included out-of-the-box:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import picsha
|
|
62
|
+
import asyncio
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
async with picsha.AsyncClient(api_key="sk_your_key_here") as client:
|
|
66
|
+
result = await client.search(query="cancer cells", mode="ai")
|
|
67
|
+
print(f"Found {result.total_count} assets.")
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
See the `notebooks/` directory for full examples integrating with Pandas, PyTorch, and SciPy.
|
|
73
|
+
|
picsha-0.1.1/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Picsha AI Python SDK 🐍
|
|
2
|
+
|
|
3
|
+
The official Python client for Picsha AI. Designed specifically for data scientists, computational biologists, and ML engineers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
We recommend using poetry:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
poetry add picsha
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or pip:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install picsha
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import picsha
|
|
23
|
+
|
|
24
|
+
client = picsha.Client(api_key="sk_your_key_here")
|
|
25
|
+
|
|
26
|
+
# 1. Semantic Search
|
|
27
|
+
results = client.search(
|
|
28
|
+
query="fluorescent cancer cell cultures",
|
|
29
|
+
mode="ai" # Hybrid Vector Search
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# 2. Get transformed URLs for ML pipelines
|
|
33
|
+
for asset in results.assets:
|
|
34
|
+
url = asset.generate_url(width=512, height=512, format="webp")
|
|
35
|
+
print(url)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Asynchronous Support
|
|
39
|
+
|
|
40
|
+
For high-throughput workloads, an `AsyncClient` is included out-of-the-box:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import picsha
|
|
44
|
+
import asyncio
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
async with picsha.AsyncClient(api_key="sk_your_key_here") as client:
|
|
48
|
+
result = await client.search(query="cancer cells", mode="ai")
|
|
49
|
+
print(f"Found {result.total_count} assets.")
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
See the `notebooks/` directory for full examples integrating with Pandas, PyTorch, and SciPy.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .client import Client, AsyncClient
|
|
2
|
+
from .exceptions import PicshaError, PicshaAuthError, PicshaAPIError
|
|
3
|
+
from .models import Asset, SearchResult, UploadResult
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Client",
|
|
9
|
+
"AsyncClient",
|
|
10
|
+
"Asset",
|
|
11
|
+
"SearchResult",
|
|
12
|
+
"UploadResult",
|
|
13
|
+
"PicshaError",
|
|
14
|
+
"PicshaAuthError",
|
|
15
|
+
"PicshaAPIError"
|
|
16
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import httpx
|
|
3
|
+
from typing import Optional, List, Dict, Any
|
|
4
|
+
|
|
5
|
+
from .exceptions import PicshaAuthError, PicshaAPIError
|
|
6
|
+
from .models import Asset, SearchResult, UploadResult
|
|
7
|
+
|
|
8
|
+
class BaseClient:
|
|
9
|
+
"""Base class for shared client configurations."""
|
|
10
|
+
DEFAULT_BASE_URL = "https://api.picsha.ai/v1"
|
|
11
|
+
|
|
12
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
13
|
+
self.api_key = api_key or os.environ.get("PICSHA_API_KEY")
|
|
14
|
+
if not self.api_key:
|
|
15
|
+
raise PicshaAuthError("API Key is missing. Pass it to the Client or set PICSHA_API_KEY env var.")
|
|
16
|
+
|
|
17
|
+
self.base_url = base_url or self.DEFAULT_BASE_URL
|
|
18
|
+
self.headers = {
|
|
19
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
20
|
+
"User-Agent": "picsha-python-sdk/0.1.0"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def _handle_error(self, response: httpx.Response):
|
|
24
|
+
if response.status_code in (401, 403):
|
|
25
|
+
raise PicshaAuthError("Unauthorized. Invalid API key or insufficient permissions.")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
error_data = response.json()
|
|
29
|
+
except Exception:
|
|
30
|
+
error_data = {"text": response.text}
|
|
31
|
+
raise PicshaAPIError(f"API Request Failed to {response.url.path}", status_code=response.status_code, details=error_data)
|
|
32
|
+
|
|
33
|
+
class Client(BaseClient):
|
|
34
|
+
"""Synchronous client for interacting with the Picsha AI API."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
37
|
+
super().__init__(api_key, base_url)
|
|
38
|
+
self._client = httpx.Client(base_url=self.base_url, headers=self.headers)
|
|
39
|
+
|
|
40
|
+
def _request(self, method: str, path: str, **kwargs) -> Any:
|
|
41
|
+
response = self._client.request(method, path, **kwargs)
|
|
42
|
+
if not response.is_success:
|
|
43
|
+
self._handle_error(response)
|
|
44
|
+
return response.json()
|
|
45
|
+
|
|
46
|
+
def search(self, query: str, mode: str = "ai", limit: int = 10, offset: int = 0) -> SearchResult:
|
|
47
|
+
data = self._request("GET", "search", params={
|
|
48
|
+
"q": query, "mode": mode, "limit": limit, "offset": offset
|
|
49
|
+
})
|
|
50
|
+
return SearchResult(**data)
|
|
51
|
+
|
|
52
|
+
def upload(self, file_path: str, tags: Optional[List[str]] = None, metadata: Optional[Dict[str, Any]] = None, ephemeral: bool = False) -> UploadResult:
|
|
53
|
+
if not os.path.exists(file_path):
|
|
54
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
55
|
+
|
|
56
|
+
filename = os.path.basename(file_path)
|
|
57
|
+
data = {}
|
|
58
|
+
if tags:
|
|
59
|
+
data["tags"] = ",".join(tags)
|
|
60
|
+
if metadata:
|
|
61
|
+
import json
|
|
62
|
+
data["metadata"] = json.dumps(metadata)
|
|
63
|
+
|
|
64
|
+
params = {}
|
|
65
|
+
if ephemeral:
|
|
66
|
+
params["ephemeral"] = "true"
|
|
67
|
+
|
|
68
|
+
with open(file_path, "rb") as f:
|
|
69
|
+
files = {"file": (filename, f)}
|
|
70
|
+
response_data = self._request("POST", "assets", params=params, data=data, files=files)
|
|
71
|
+
|
|
72
|
+
return UploadResult(**response_data)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AsyncClient(BaseClient):
|
|
76
|
+
"""Asynchronous client for interacting with the Picsha AI API."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
79
|
+
super().__init__(api_key, base_url)
|
|
80
|
+
self._client = httpx.AsyncClient(base_url=self.base_url, headers=self.headers)
|
|
81
|
+
|
|
82
|
+
async def _request(self, method: str, path: str, **kwargs) -> Any:
|
|
83
|
+
response = await self._client.request(method, path, **kwargs)
|
|
84
|
+
if not response.is_success:
|
|
85
|
+
self._handle_error(response)
|
|
86
|
+
return response.json()
|
|
87
|
+
|
|
88
|
+
async def search(self, query: str, mode: str = "ai", limit: int = 10, offset: int = 0) -> SearchResult:
|
|
89
|
+
data = await self._request("GET", "search", params={
|
|
90
|
+
"q": query, "mode": mode, "limit": limit, "offset": offset
|
|
91
|
+
})
|
|
92
|
+
return SearchResult(**data)
|
|
93
|
+
|
|
94
|
+
async def upload(self, file_path: str, tags: Optional[List[str]] = None, metadata: Optional[Dict[str, Any]] = None, ephemeral: bool = False) -> UploadResult:
|
|
95
|
+
if not os.path.exists(file_path):
|
|
96
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
97
|
+
|
|
98
|
+
filename = os.path.basename(file_path)
|
|
99
|
+
data = {}
|
|
100
|
+
if tags:
|
|
101
|
+
data["tags"] = ",".join(tags)
|
|
102
|
+
if metadata:
|
|
103
|
+
import json
|
|
104
|
+
data["metadata"] = json.dumps(metadata)
|
|
105
|
+
|
|
106
|
+
params = {}
|
|
107
|
+
if ephemeral:
|
|
108
|
+
params["ephemeral"] = "true"
|
|
109
|
+
|
|
110
|
+
with open(file_path, "rb") as f:
|
|
111
|
+
# We read bytes into memory for V1 async simplified uploads via httpx
|
|
112
|
+
file_bytes = f.read()
|
|
113
|
+
|
|
114
|
+
files = {"file": (filename, file_bytes)}
|
|
115
|
+
response_data = await self._request("POST", "assets", params=params, data=data, files=files)
|
|
116
|
+
|
|
117
|
+
return UploadResult(**response_data)
|
|
118
|
+
|
|
119
|
+
async def close(self):
|
|
120
|
+
"""Close the underlying async httpx client."""
|
|
121
|
+
await self._client.aclose()
|
|
122
|
+
|
|
123
|
+
async def __aenter__(self):
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
127
|
+
await self.close()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class PicshaError(Exception):
|
|
2
|
+
"""Base exception for Picsha SDK"""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class PicshaAuthError(PicshaError):
|
|
6
|
+
"""Raised when an API Key is invalid or missing"""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class PicshaAPIError(PicshaError):
|
|
10
|
+
"""Raised when the API returns an error response"""
|
|
11
|
+
def __init__(self, message: str, status_code: int = None, details: dict = None):
|
|
12
|
+
super().__init__(f"{message} (HTTP {status_code})")
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.details = details or {}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import List, Optional, Dict, Any
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
class AssetTransformationOptions(BaseModel):
|
|
5
|
+
width: Optional[int] = None
|
|
6
|
+
height: Optional[int] = None
|
|
7
|
+
format: Optional[str] = None
|
|
8
|
+
quality: Optional[int] = None
|
|
9
|
+
crop: Optional[str] = None
|
|
10
|
+
|
|
11
|
+
class Asset(BaseModel):
|
|
12
|
+
id: str
|
|
13
|
+
url: str
|
|
14
|
+
original_filename: Optional[str] = None
|
|
15
|
+
mime_type: Optional[str] = None
|
|
16
|
+
size_bytes: Optional[int] = None
|
|
17
|
+
tags: List[str] = Field(default_factory=list)
|
|
18
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
def generate_url(self, **kwargs) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Generate a transformed image URL locally (similar to Cloudinary SDK).
|
|
23
|
+
Example: asset.generate_url(width=512, format="webp")
|
|
24
|
+
"""
|
|
25
|
+
# A simple naive implementation, but matches a CDN proxy approach
|
|
26
|
+
# For Picsha, we might pass params as query string to our delivery pipeline
|
|
27
|
+
from urllib.parse import urlparse, urlencode, urlunparse
|
|
28
|
+
|
|
29
|
+
parsed = urlparse(self.url)
|
|
30
|
+
query_params = {}
|
|
31
|
+
if parsed.query:
|
|
32
|
+
# simple parsing just for demo
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
if kwargs:
|
|
36
|
+
query_params.update(kwargs)
|
|
37
|
+
|
|
38
|
+
new_query = urlencode(query_params)
|
|
39
|
+
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, new_query, parsed.fragment))
|
|
40
|
+
|
|
41
|
+
class SearchResult(BaseModel):
|
|
42
|
+
assets: List[Asset]
|
|
43
|
+
total_count: int
|
|
44
|
+
has_more: bool
|
|
45
|
+
|
|
46
|
+
class UploadResult(BaseModel):
|
|
47
|
+
success: bool
|
|
48
|
+
asset: Optional[Asset] = None
|
|
49
|
+
message: Optional[str] = None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "picsha"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Official Python SDK for Picsha AI - Store, Encode, Understand."
|
|
5
|
+
authors = ["Peter <peter@picsha.ai>"] # Assumed from path, will configure accordingly
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "picsha"}]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.9"
|
|
11
|
+
httpx = "^0.27.0"
|
|
12
|
+
pydantic = "^2.6.0"
|
|
13
|
+
|
|
14
|
+
[tool.poetry.group.dev.dependencies]
|
|
15
|
+
pytest = "^8.0.0"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["poetry-core"]
|
|
19
|
+
build-backend = "poetry.core.masonry.api"
|