quercle 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.
- quercle-0.1.0/.github/workflows/publish.yml +43 -0
- quercle-0.1.0/.gitignore +49 -0
- quercle-0.1.0/CLAUDE.md +491 -0
- quercle-0.1.0/LICENSE +21 -0
- quercle-0.1.0/PKG-INFO +198 -0
- quercle-0.1.0/README.md +169 -0
- quercle-0.1.0/pyproject.toml +55 -0
- quercle-0.1.0/quercle/__init__.py +21 -0
- quercle-0.1.0/quercle/_base.py +86 -0
- quercle-0.1.0/quercle/async_client.py +86 -0
- quercle-0.1.0/quercle/client.py +86 -0
- quercle-0.1.0/quercle/exceptions.py +34 -0
- quercle-0.1.0/quercle/py.typed +0 -0
- quercle-0.1.0/tests/conftest.py +91 -0
- quercle-0.1.0/tests/test_async_client.py +213 -0
- quercle-0.1.0/tests/test_client.py +194 -0
- quercle-0.1.0/uv.lock +537 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
paths:
|
|
7
|
+
- 'quercle/**'
|
|
8
|
+
- 'pyproject.toml'
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write # Required for trusted publishing
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v4
|
|
21
|
+
with:
|
|
22
|
+
version: "latest"
|
|
23
|
+
|
|
24
|
+
- name: Set up Python
|
|
25
|
+
run: uv python install 3.12
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync --all-extras
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: uv run pytest
|
|
32
|
+
|
|
33
|
+
- name: Run lints
|
|
34
|
+
run: |
|
|
35
|
+
uv run ruff check .
|
|
36
|
+
uv run mypy quercle
|
|
37
|
+
|
|
38
|
+
- name: Build package
|
|
39
|
+
run: uv build
|
|
40
|
+
|
|
41
|
+
- name: Publish to PyPI
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
43
|
+
# Uses trusted publishing - configure in PyPI project settings
|
quercle-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
31
|
+
*.swp
|
|
32
|
+
*.swo
|
|
33
|
+
|
|
34
|
+
# Testing
|
|
35
|
+
.pytest_cache/
|
|
36
|
+
.coverage
|
|
37
|
+
htmlcov/
|
|
38
|
+
.tox/
|
|
39
|
+
.nox/
|
|
40
|
+
|
|
41
|
+
# mypy
|
|
42
|
+
.mypy_cache/
|
|
43
|
+
|
|
44
|
+
# ruff
|
|
45
|
+
.ruff_cache/
|
|
46
|
+
|
|
47
|
+
# OS
|
|
48
|
+
.DS_Store
|
|
49
|
+
Thumbs.db
|
quercle-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
# CLAUDE.md - Quercle Python SDK
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
Official Python SDK for the Quercle API. Provides both **synchronous** and **asynchronous** clients for AI-powered web search and URL fetching.
|
|
6
|
+
|
|
7
|
+
This is the core library that framework integrations (LangChain, CrewAI, LlamaIndex, etc.) depend on.
|
|
8
|
+
|
|
9
|
+
## Development Guidelines
|
|
10
|
+
|
|
11
|
+
**IMPORTANT:**
|
|
12
|
+
- Always use the **latest stable versions** of all dependencies
|
|
13
|
+
- Use **`uv`** for Python package management (NOT pip)
|
|
14
|
+
- Use modern Python patterns, type hints, and async/await
|
|
15
|
+
- Check PyPI for current versions before specifying dependencies
|
|
16
|
+
- Keep sync and async implementations DRY (shared logic, thin wrappers)
|
|
17
|
+
|
|
18
|
+
## Quercle API
|
|
19
|
+
|
|
20
|
+
### Authentication
|
|
21
|
+
- Header: `X-API-Key: qk_...`
|
|
22
|
+
- Env var: `QUERCLE_API_KEY`
|
|
23
|
+
- Get API key at: https://quercle.dev
|
|
24
|
+
|
|
25
|
+
### Endpoints
|
|
26
|
+
|
|
27
|
+
**POST https://quercle.dev/api/v1/fetch**
|
|
28
|
+
|
|
29
|
+
Fetch a web page and analyze its content using AI. Provide a URL and a prompt describing what information you want to extract or how to analyze the content. The raw HTML is NOT returned - only the AI's analysis based on your prompt.
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
// Request
|
|
33
|
+
{
|
|
34
|
+
"url": "https://example.com",
|
|
35
|
+
"prompt": "Summarize the main points of this page"
|
|
36
|
+
}
|
|
37
|
+
// Response
|
|
38
|
+
{"result": "AI-processed content..."}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**POST https://quercle.dev/api/v1/search**
|
|
42
|
+
|
|
43
|
+
Search the web and get an AI-synthesized answer with citations. The response includes the answer and source URLs that can be fetched for further investigation. Optionally filter by allowed or blocked domains.
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
// Request
|
|
47
|
+
{
|
|
48
|
+
"query": "What is TypeScript?",
|
|
49
|
+
"allowed_domains": ["*.edu", "*.gov"], // optional
|
|
50
|
+
"blocked_domains": ["spam.com"] // optional
|
|
51
|
+
}
|
|
52
|
+
// Response
|
|
53
|
+
{"result": "Synthesized answer with [1] citations...\n\nSources:\n[1] Title - URL"}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Error Codes
|
|
57
|
+
- `400` - Invalid request (bad URL, empty prompt)
|
|
58
|
+
- `401` - Invalid or missing API key
|
|
59
|
+
- `402` - Insufficient credits
|
|
60
|
+
- `403` - Account inactive
|
|
61
|
+
- `404` - Resource not found
|
|
62
|
+
- `504` - Request timeout
|
|
63
|
+
|
|
64
|
+
## Package Structure
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
quercle/
|
|
68
|
+
├── __init__.py # Exports QuercleClient, AsyncQuercleClient, QuercleError
|
|
69
|
+
├── client.py # Sync client implementation
|
|
70
|
+
├── async_client.py # Async client implementation
|
|
71
|
+
├── _base.py # Shared logic (DRY - both clients use this)
|
|
72
|
+
├── types.py # Type definitions, response models
|
|
73
|
+
├── exceptions.py # QuercleError and specific exceptions
|
|
74
|
+
└── py.typed # PEP 561 marker
|
|
75
|
+
tests/
|
|
76
|
+
├── test_client.py # Sync client tests
|
|
77
|
+
├── test_async_client.py # Async client tests
|
|
78
|
+
└── conftest.py # Shared fixtures
|
|
79
|
+
pyproject.toml
|
|
80
|
+
README.md
|
|
81
|
+
LICENSE # MIT
|
|
82
|
+
.github/
|
|
83
|
+
└── workflows/
|
|
84
|
+
└── publish.yml # Auto-publish to PyPI on merge to master
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Implementation Details
|
|
88
|
+
|
|
89
|
+
### Shared Base (DRY Pattern)
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# quercle/_base.py
|
|
93
|
+
from typing import Any
|
|
94
|
+
|
|
95
|
+
class BaseQuercleClient:
|
|
96
|
+
"""Shared logic for sync and async clients."""
|
|
97
|
+
|
|
98
|
+
DEFAULT_BASE_URL = "https://quercle.dev"
|
|
99
|
+
DEFAULT_TIMEOUT = 120.0 # seconds
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
api_key: str | None = None,
|
|
104
|
+
base_url: str | None = None,
|
|
105
|
+
timeout: float | None = None,
|
|
106
|
+
):
|
|
107
|
+
self.api_key = api_key or os.environ.get("QUERCLE_API_KEY")
|
|
108
|
+
if not self.api_key:
|
|
109
|
+
raise QuercleError("API key required. Get one at https://quercle.dev", 401)
|
|
110
|
+
self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
|
|
111
|
+
self.timeout = timeout or self.DEFAULT_TIMEOUT
|
|
112
|
+
|
|
113
|
+
def _build_headers(self) -> dict[str, str]:
|
|
114
|
+
return {
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
"X-API-Key": self.api_key,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def _handle_error(self, status_code: int, detail: str) -> None:
|
|
120
|
+
"""Raise appropriate exception based on status code."""
|
|
121
|
+
# Shared error handling logic
|
|
122
|
+
...
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Sync Client
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# quercle/client.py
|
|
129
|
+
import httpx
|
|
130
|
+
from quercle._base import BaseQuercleClient
|
|
131
|
+
|
|
132
|
+
class QuercleClient(BaseQuercleClient):
|
|
133
|
+
"""Synchronous Quercle API client."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, **kwargs):
|
|
136
|
+
super().__init__(**kwargs)
|
|
137
|
+
self._client = httpx.Client(timeout=self.timeout)
|
|
138
|
+
|
|
139
|
+
def fetch(self, url: str, prompt: str) -> str:
|
|
140
|
+
"""Fetch a URL and analyze with AI.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
url: The URL to fetch and analyze
|
|
144
|
+
prompt: Instructions for how to analyze the page content.
|
|
145
|
+
Be specific about what information you want to extract.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
AI-processed analysis of the page content
|
|
149
|
+
"""
|
|
150
|
+
response = self._client.post(
|
|
151
|
+
f"{self.base_url}/api/v1/fetch",
|
|
152
|
+
headers=self._build_headers(),
|
|
153
|
+
json={"url": url, "prompt": prompt},
|
|
154
|
+
)
|
|
155
|
+
self._handle_response(response)
|
|
156
|
+
return response.json()["result"]
|
|
157
|
+
|
|
158
|
+
def search(
|
|
159
|
+
self,
|
|
160
|
+
query: str,
|
|
161
|
+
*,
|
|
162
|
+
allowed_domains: list[str] | None = None,
|
|
163
|
+
blocked_domains: list[str] | None = None,
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Search the web and get AI-synthesized answers.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
query: The search query to find information about. Be specific.
|
|
169
|
+
allowed_domains: Only include results from these domains
|
|
170
|
+
(e.g., ['example.com', '*.example.org'])
|
|
171
|
+
blocked_domains: Exclude results from these domains
|
|
172
|
+
(e.g., ['example.com', '*.example.org'])
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
AI-synthesized answer with source citations
|
|
176
|
+
"""
|
|
177
|
+
body = {"query": query}
|
|
178
|
+
if allowed_domains:
|
|
179
|
+
body["allowed_domains"] = allowed_domains
|
|
180
|
+
if blocked_domains:
|
|
181
|
+
body["blocked_domains"] = blocked_domains
|
|
182
|
+
|
|
183
|
+
response = self._client.post(
|
|
184
|
+
f"{self.base_url}/api/v1/search",
|
|
185
|
+
headers=self._build_headers(),
|
|
186
|
+
json=body,
|
|
187
|
+
)
|
|
188
|
+
self._handle_response(response)
|
|
189
|
+
return response.json()["result"]
|
|
190
|
+
|
|
191
|
+
def close(self) -> None:
|
|
192
|
+
self._client.close()
|
|
193
|
+
|
|
194
|
+
def __enter__(self):
|
|
195
|
+
return self
|
|
196
|
+
|
|
197
|
+
def __exit__(self, *args):
|
|
198
|
+
self.close()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Async Client
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
# quercle/async_client.py
|
|
205
|
+
import httpx
|
|
206
|
+
from quercle._base import BaseQuercleClient
|
|
207
|
+
|
|
208
|
+
class AsyncQuercleClient(BaseQuercleClient):
|
|
209
|
+
"""Asynchronous Quercle API client."""
|
|
210
|
+
|
|
211
|
+
def __init__(self, **kwargs):
|
|
212
|
+
super().__init__(**kwargs)
|
|
213
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
214
|
+
|
|
215
|
+
async def fetch(self, url: str, prompt: str) -> str:
|
|
216
|
+
"""Fetch a URL and analyze with AI (async)."""
|
|
217
|
+
response = await self._client.post(
|
|
218
|
+
f"{self.base_url}/api/v1/fetch",
|
|
219
|
+
headers=self._build_headers(),
|
|
220
|
+
json={"url": url, "prompt": prompt},
|
|
221
|
+
)
|
|
222
|
+
self._handle_response(response)
|
|
223
|
+
return response.json()["result"]
|
|
224
|
+
|
|
225
|
+
async def search(
|
|
226
|
+
self,
|
|
227
|
+
query: str,
|
|
228
|
+
*,
|
|
229
|
+
allowed_domains: list[str] | None = None,
|
|
230
|
+
blocked_domains: list[str] | None = None,
|
|
231
|
+
) -> str:
|
|
232
|
+
"""Search the web and get AI-synthesized answers (async)."""
|
|
233
|
+
body = {"query": query}
|
|
234
|
+
if allowed_domains:
|
|
235
|
+
body["allowed_domains"] = allowed_domains
|
|
236
|
+
if blocked_domains:
|
|
237
|
+
body["blocked_domains"] = blocked_domains
|
|
238
|
+
|
|
239
|
+
response = await self._client.post(
|
|
240
|
+
f"{self.base_url}/api/v1/search",
|
|
241
|
+
headers=self._build_headers(),
|
|
242
|
+
json=body,
|
|
243
|
+
)
|
|
244
|
+
self._handle_response(response)
|
|
245
|
+
return response.json()["result"]
|
|
246
|
+
|
|
247
|
+
async def close(self) -> None:
|
|
248
|
+
await self._client.aclose()
|
|
249
|
+
|
|
250
|
+
async def __aenter__(self):
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
async def __aexit__(self, *args):
|
|
254
|
+
await self.close()
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Exceptions
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
# quercle/exceptions.py
|
|
261
|
+
class QuercleError(Exception):
|
|
262
|
+
"""Base exception for Quercle API errors."""
|
|
263
|
+
|
|
264
|
+
def __init__(self, message: str, status_code: int, detail: str | None = None):
|
|
265
|
+
super().__init__(message)
|
|
266
|
+
self.status_code = status_code
|
|
267
|
+
self.detail = detail
|
|
268
|
+
|
|
269
|
+
class AuthenticationError(QuercleError):
|
|
270
|
+
"""Invalid or missing API key (401)."""
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
class InsufficientCreditsError(QuercleError):
|
|
274
|
+
"""Not enough credits (402)."""
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
class RateLimitError(QuercleError):
|
|
278
|
+
"""Too many requests (429)."""
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
class TimeoutError(QuercleError):
|
|
282
|
+
"""Request timed out (504)."""
|
|
283
|
+
pass
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Commands
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# Install dependencies
|
|
290
|
+
uv sync
|
|
291
|
+
|
|
292
|
+
# Run tests
|
|
293
|
+
uv run pytest
|
|
294
|
+
|
|
295
|
+
# Run tests with coverage
|
|
296
|
+
uv run pytest --cov=quercle
|
|
297
|
+
|
|
298
|
+
# Lint
|
|
299
|
+
uv run ruff check .
|
|
300
|
+
|
|
301
|
+
# Format
|
|
302
|
+
uv run ruff format .
|
|
303
|
+
|
|
304
|
+
# Type check
|
|
305
|
+
uv run mypy quercle
|
|
306
|
+
|
|
307
|
+
# Build package
|
|
308
|
+
uv build
|
|
309
|
+
|
|
310
|
+
# Publish to PyPI (manual)
|
|
311
|
+
uv publish
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Dependencies
|
|
315
|
+
|
|
316
|
+
- Python 3.10+
|
|
317
|
+
- httpx (HTTP client with async support)
|
|
318
|
+
- pydantic (optional, for response validation)
|
|
319
|
+
|
|
320
|
+
## pyproject.toml
|
|
321
|
+
|
|
322
|
+
```toml
|
|
323
|
+
[project]
|
|
324
|
+
name = "quercle"
|
|
325
|
+
version = "0.1.0"
|
|
326
|
+
description = "Official Python SDK for the Quercle API - AI-powered web search and fetching"
|
|
327
|
+
readme = "README.md"
|
|
328
|
+
license = { text = "MIT" }
|
|
329
|
+
requires-python = ">=3.10"
|
|
330
|
+
authors = [{ name = "Quercle", email = "support@quercle.dev" }]
|
|
331
|
+
keywords = ["quercle", "api", "ai", "search", "web", "fetch"]
|
|
332
|
+
classifiers = [
|
|
333
|
+
"Development Status :: 4 - Beta",
|
|
334
|
+
"Intended Audience :: Developers",
|
|
335
|
+
"License :: OSI Approved :: MIT License",
|
|
336
|
+
"Programming Language :: Python :: 3",
|
|
337
|
+
"Programming Language :: Python :: 3.10",
|
|
338
|
+
"Programming Language :: Python :: 3.11",
|
|
339
|
+
"Programming Language :: Python :: 3.12",
|
|
340
|
+
"Typing :: Typed",
|
|
341
|
+
]
|
|
342
|
+
dependencies = [
|
|
343
|
+
"httpx>=0.27.0",
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
[project.optional-dependencies]
|
|
347
|
+
dev = [
|
|
348
|
+
"pytest>=8.0.0",
|
|
349
|
+
"pytest-asyncio>=0.23.0",
|
|
350
|
+
"pytest-cov>=4.1.0",
|
|
351
|
+
"ruff>=0.4.0",
|
|
352
|
+
"mypy>=1.10.0",
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
[project.urls]
|
|
356
|
+
Homepage = "https://quercle.dev"
|
|
357
|
+
Documentation = "https://quercle.dev/docs"
|
|
358
|
+
Repository = "https://github.com/quercledev/quercle-python"
|
|
359
|
+
|
|
360
|
+
[build-system]
|
|
361
|
+
requires = ["hatchling"]
|
|
362
|
+
build-backend = "hatchling.build"
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Usage Examples
|
|
366
|
+
|
|
367
|
+
### Sync Client
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
from quercle import QuercleClient
|
|
371
|
+
|
|
372
|
+
# Using environment variable QUERCLE_API_KEY
|
|
373
|
+
client = QuercleClient()
|
|
374
|
+
|
|
375
|
+
# Or explicit API key
|
|
376
|
+
client = QuercleClient(api_key="qk_...")
|
|
377
|
+
|
|
378
|
+
# Fetch and analyze a URL
|
|
379
|
+
result = client.fetch(
|
|
380
|
+
url="https://example.com/article",
|
|
381
|
+
prompt="Summarize the main points in bullet points"
|
|
382
|
+
)
|
|
383
|
+
print(result)
|
|
384
|
+
|
|
385
|
+
# Search the web
|
|
386
|
+
result = client.search("What is TypeScript?")
|
|
387
|
+
print(result)
|
|
388
|
+
|
|
389
|
+
# Search with domain filtering
|
|
390
|
+
result = client.search(
|
|
391
|
+
"Python best practices",
|
|
392
|
+
allowed_domains=["*.python.org", "realpython.com"]
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Context manager
|
|
396
|
+
with QuercleClient() as client:
|
|
397
|
+
result = client.fetch("https://example.com", "Extract the title")
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Async Client
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
import asyncio
|
|
404
|
+
from quercle import AsyncQuercleClient
|
|
405
|
+
|
|
406
|
+
async def main():
|
|
407
|
+
async with AsyncQuercleClient() as client:
|
|
408
|
+
# Fetch
|
|
409
|
+
result = await client.fetch(
|
|
410
|
+
url="https://example.com",
|
|
411
|
+
prompt="Summarize this page"
|
|
412
|
+
)
|
|
413
|
+
print(result)
|
|
414
|
+
|
|
415
|
+
# Search
|
|
416
|
+
result = await client.search("Latest AI news")
|
|
417
|
+
print(result)
|
|
418
|
+
|
|
419
|
+
# Parallel requests
|
|
420
|
+
results = await asyncio.gather(
|
|
421
|
+
client.search("Python tutorials"),
|
|
422
|
+
client.search("TypeScript tutorials"),
|
|
423
|
+
)
|
|
424
|
+
for r in results:
|
|
425
|
+
print(r)
|
|
426
|
+
|
|
427
|
+
asyncio.run(main())
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## CI/CD - Auto-publish to PyPI
|
|
431
|
+
|
|
432
|
+
Create `.github/workflows/publish.yml`:
|
|
433
|
+
|
|
434
|
+
```yaml
|
|
435
|
+
name: Publish to PyPI
|
|
436
|
+
|
|
437
|
+
on:
|
|
438
|
+
push:
|
|
439
|
+
branches: [master]
|
|
440
|
+
paths:
|
|
441
|
+
- 'quercle/**'
|
|
442
|
+
- 'pyproject.toml'
|
|
443
|
+
|
|
444
|
+
jobs:
|
|
445
|
+
publish:
|
|
446
|
+
runs-on: ubuntu-latest
|
|
447
|
+
permissions:
|
|
448
|
+
id-token: write # Required for trusted publishing
|
|
449
|
+
|
|
450
|
+
steps:
|
|
451
|
+
- uses: actions/checkout@v4
|
|
452
|
+
|
|
453
|
+
- name: Install uv
|
|
454
|
+
uses: astral-sh/setup-uv@v4
|
|
455
|
+
with:
|
|
456
|
+
version: "latest"
|
|
457
|
+
|
|
458
|
+
- name: Set up Python
|
|
459
|
+
run: uv python install 3.12
|
|
460
|
+
|
|
461
|
+
- name: Install dependencies
|
|
462
|
+
run: uv sync --all-extras
|
|
463
|
+
|
|
464
|
+
- name: Run tests
|
|
465
|
+
run: uv run pytest
|
|
466
|
+
|
|
467
|
+
- name: Run lints
|
|
468
|
+
run: |
|
|
469
|
+
uv run ruff check .
|
|
470
|
+
uv run mypy quercle
|
|
471
|
+
|
|
472
|
+
- name: Build package
|
|
473
|
+
run: uv build
|
|
474
|
+
|
|
475
|
+
- name: Publish to PyPI
|
|
476
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
477
|
+
# Uses trusted publishing - configure in PyPI project settings
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## Publishing Setup
|
|
481
|
+
|
|
482
|
+
1. Create PyPI account at https://pypi.org
|
|
483
|
+
2. Create project `quercle`
|
|
484
|
+
3. Go to project settings → "Publishing" → "Add a new pending publisher"
|
|
485
|
+
4. Configure:
|
|
486
|
+
- Owner: `quercledev`
|
|
487
|
+
- Repository: `quercle-python`
|
|
488
|
+
- Workflow: `publish.yml`
|
|
489
|
+
- Environment: (leave blank)
|
|
490
|
+
|
|
491
|
+
Package name on PyPI: `quercle`
|
quercle-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Quercle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|