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 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"