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.
@@ -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
@@ -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
@@ -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.