liyaengine 0.1.0__py3-none-any.whl

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.
liyaengine/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ from .client import LiyaEngine
2
+ from .errors import LiyaEngineAPIError, LiyaEngineNetworkError
3
+ from .resources.collections import Collection
4
+
5
+ __all__ = [
6
+ "LiyaEngine",
7
+ "LiyaEngineAPIError",
8
+ "LiyaEngineNetworkError",
9
+ "Collection",
10
+ ]
11
+
12
+ __version__ = "0.1.0"
liyaengine/_http.py ADDED
@@ -0,0 +1,96 @@
1
+ """Thin httpx wrapper: bearer auth, JSON in/out, the {success,data} /
2
+ {success,error} envelope unwrapped into a return value or a raised
3
+ LiyaEngineAPIError, and retry-with-backoff on 429/5xx (not on 4xx, which
4
+ are the caller's own mistake and won't succeed on retry).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from typing import Any, Dict, Optional
10
+
11
+ import httpx
12
+
13
+ from .errors import LiyaEngineAPIError, LiyaEngineNetworkError
14
+
15
+ _DEFAULT_TIMEOUT_S = 30.0
16
+ _DEFAULT_MAX_RETRIES = 2
17
+ _RETRYABLE_STATUS = {429, 500, 502, 503, 504}
18
+
19
+
20
+ class HttpClient:
21
+ def __init__(
22
+ self,
23
+ api_key: str,
24
+ base_url: str,
25
+ timeout_s: float = _DEFAULT_TIMEOUT_S,
26
+ max_retries: int = _DEFAULT_MAX_RETRIES,
27
+ client: Optional[httpx.Client] = None,
28
+ ) -> None:
29
+ self._max_retries = max_retries
30
+ self._client = client or httpx.Client(
31
+ base_url=base_url.rstrip("/"),
32
+ timeout=timeout_s,
33
+ headers={
34
+ "Authorization": f"Bearer {api_key}",
35
+ "Content-Type": "application/json",
36
+ },
37
+ )
38
+
39
+ def request(self, method: str, path: str, json_body: Optional[Dict[str, Any]] = None) -> Any:
40
+ last_error: Optional[BaseException] = None
41
+
42
+ for attempt in range(self._max_retries + 1):
43
+ try:
44
+ response = self._client.request(method, path, json=json_body)
45
+ except httpx.TimeoutException as exc:
46
+ last_error = LiyaEngineNetworkError(f"Request timed out: {exc}", exc)
47
+ if attempt >= self._max_retries:
48
+ raise last_error from exc
49
+ time.sleep(2**attempt * 0.25)
50
+ continue
51
+ except httpx.RequestError as exc:
52
+ last_error = LiyaEngineNetworkError(f"Network request failed: {exc}", exc)
53
+ if attempt >= self._max_retries:
54
+ raise last_error from exc
55
+ time.sleep(2**attempt * 0.25)
56
+ continue
57
+
58
+ if response.status_code in _RETRYABLE_STATUS and attempt < self._max_retries:
59
+ time.sleep(2**attempt * 0.25)
60
+ continue
61
+
62
+ try:
63
+ payload = response.json()
64
+ except ValueError as exc:
65
+ raise LiyaEngineNetworkError(
66
+ f"Invalid JSON response (status {response.status_code})", exc
67
+ ) from exc
68
+
69
+ if not payload.get("success"):
70
+ error = payload.get("error", {})
71
+ raise LiyaEngineAPIError(
72
+ response.status_code,
73
+ error.get("code", "UNKNOWN_ERROR"),
74
+ error.get("message", "Unknown error"),
75
+ error.get("details"),
76
+ )
77
+ return payload.get("data")
78
+
79
+ if last_error is not None:
80
+ raise last_error
81
+ raise LiyaEngineNetworkError("Request failed")
82
+
83
+ def get(self, path: str) -> Any:
84
+ return self.request("GET", path)
85
+
86
+ def post(self, path: str, json_body: Optional[Dict[str, Any]] = None) -> Any:
87
+ return self.request("POST", path, json_body)
88
+
89
+ def patch(self, path: str, json_body: Optional[Dict[str, Any]] = None) -> Any:
90
+ return self.request("PATCH", path, json_body)
91
+
92
+ def delete(self, path: str) -> Any:
93
+ return self.request("DELETE", path)
94
+
95
+ def close(self) -> None:
96
+ self._client.close()
liyaengine/client.py ADDED
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import httpx
6
+
7
+ from ._http import HttpClient
8
+ from .resources.collections import CollectionsResource
9
+
10
+ _DEFAULT_BASE_URL = "https://api.liyaengine.ai"
11
+
12
+
13
+ class LiyaEngine:
14
+ """Client for the Liya Engine public API.
15
+
16
+ Example:
17
+ >>> client = LiyaEngine(api_key="liya_...")
18
+ >>> collection = client.collections.create(
19
+ ... slug="contracts", label="Contracts", domain_keys=["legal-ops"]
20
+ ... )
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ api_key: str,
26
+ base_url: str = _DEFAULT_BASE_URL,
27
+ timeout_s: float = 30.0,
28
+ max_retries: int = 2,
29
+ http_client: Optional[httpx.Client] = None,
30
+ ) -> None:
31
+ if not api_key:
32
+ raise ValueError("LiyaEngine: api_key is required.")
33
+
34
+ self._http = HttpClient(
35
+ api_key=api_key,
36
+ base_url=base_url,
37
+ timeout_s=timeout_s,
38
+ max_retries=max_retries,
39
+ client=http_client,
40
+ )
41
+ self.collections = CollectionsResource(self._http)
42
+
43
+ def close(self) -> None:
44
+ self._http.close()
45
+
46
+ def __enter__(self) -> "LiyaEngine":
47
+ return self
48
+
49
+ def __exit__(self, *exc_info: object) -> None:
50
+ self.close()
liyaengine/errors.py ADDED
@@ -0,0 +1,29 @@
1
+ """Every non-2xx /v1 response carries {"success": false, "error": {"code", "message"}}
2
+ (see liyaengine-api's ErrorEnvelope in openapi.yaml). This maps that envelope onto
3
+ real exceptions instead of a plain dict, so callers can `except LiyaEngineAPIError`.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Optional
8
+
9
+
10
+ class LiyaEngineAPIError(Exception):
11
+ """The API returned a well-formed error envelope."""
12
+
13
+ def __init__(self, status: int, code: str, message: str, details: Optional[Any] = None) -> None:
14
+ super().__init__(message)
15
+ self.status = status
16
+ self.code = code
17
+ self.message = message
18
+ self.details = details
19
+
20
+ def __repr__(self) -> str:
21
+ return f"LiyaEngineAPIError(status={self.status}, code={self.code!r}, message={self.message!r})"
22
+
23
+
24
+ class LiyaEngineNetworkError(Exception):
25
+ """The request never reached the server, timed out, or the response wasn't valid JSON."""
26
+
27
+ def __init__(self, message: str, cause: Optional[BaseException] = None) -> None:
28
+ super().__init__(message)
29
+ self.cause = cause
liyaengine/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from .._http import HttpClient
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Collection:
11
+ id: str
12
+ slug: str
13
+ label: str
14
+ color: str
15
+ created_at: str
16
+ domain_keys: List[str]
17
+ tags: List[str]
18
+ visibility: str
19
+ last_synced_at: Optional[str]
20
+ retrieval_config: Optional[Dict[str, Any]]
21
+ default_embedding_model: Optional[str]
22
+ default_chunking_strategy: Optional[str]
23
+ default_chunk_size: Optional[int]
24
+ default_chunk_overlap: Optional[int]
25
+
26
+ @classmethod
27
+ def _from_dict(cls, data: Dict[str, Any]) -> "Collection":
28
+ return cls(
29
+ id=data["id"],
30
+ slug=data["slug"],
31
+ label=data["label"],
32
+ color=data["color"],
33
+ created_at=data["created_at"],
34
+ domain_keys=data.get("domain_keys", []),
35
+ tags=data.get("tags", []),
36
+ visibility=data.get("visibility", "workspace"),
37
+ last_synced_at=data.get("last_synced_at"),
38
+ retrieval_config=data.get("retrieval_config"),
39
+ default_embedding_model=data.get("default_embedding_model"),
40
+ default_chunking_strategy=data.get("default_chunking_strategy"),
41
+ default_chunk_size=data.get("default_chunk_size"),
42
+ default_chunk_overlap=data.get("default_chunk_overlap"),
43
+ )
44
+
45
+
46
+ class CollectionsResource:
47
+ """Tenant-wide knowledge collections — organize documents, scope
48
+ retrieval, attach to one or many domains. Mirrors GET/POST
49
+ /v1/collections and GET/PATCH/DELETE /v1/collections/{id} exactly
50
+ (see liyaengine-api's openapi.yaml).
51
+ """
52
+
53
+ def __init__(self, http: HttpClient) -> None:
54
+ self._http = http
55
+
56
+ def list(self) -> List[Collection]:
57
+ data = self._http.get("/v1/collections")
58
+ return [Collection._from_dict(c) for c in data["collections"]]
59
+
60
+ def get(self, id: str) -> Collection:
61
+ data = self._http.get(f"/v1/collections/{id}")
62
+ return Collection._from_dict(data["collection"])
63
+
64
+ def create(
65
+ self,
66
+ *,
67
+ slug: str,
68
+ label: str,
69
+ domain_keys: List[str],
70
+ color: Optional[str] = None,
71
+ default_embedding_model: Optional[str] = None,
72
+ default_chunking_strategy: Optional[str] = None,
73
+ default_chunk_size: Optional[int] = None,
74
+ default_chunk_overlap: Optional[int] = None,
75
+ ) -> Collection:
76
+ body: Dict[str, Any] = {"slug": slug, "label": label, "domain_keys": domain_keys}
77
+ if color is not None:
78
+ body["color"] = color
79
+ if default_embedding_model is not None:
80
+ body["default_embedding_model"] = default_embedding_model
81
+ if default_chunking_strategy is not None:
82
+ body["default_chunking_strategy"] = default_chunking_strategy
83
+ if default_chunk_size is not None:
84
+ body["default_chunk_size"] = default_chunk_size
85
+ if default_chunk_overlap is not None:
86
+ body["default_chunk_overlap"] = default_chunk_overlap
87
+
88
+ data = self._http.post("/v1/collections", body)
89
+ return Collection._from_dict(data["collection"])
90
+
91
+ def update(
92
+ self,
93
+ id: str,
94
+ *,
95
+ label: Optional[str] = None,
96
+ color: Optional[str] = None,
97
+ tags: Optional[List[str]] = None,
98
+ visibility: Optional[str] = None,
99
+ retrieval_config: Optional[Dict[str, Any]] = None,
100
+ default_embedding_model: Optional[str] = None,
101
+ default_chunking_strategy: Optional[str] = None,
102
+ default_chunk_size: Optional[int] = None,
103
+ default_chunk_overlap: Optional[int] = None,
104
+ ) -> Collection:
105
+ body: Dict[str, Any] = {}
106
+ if label is not None:
107
+ body["label"] = label
108
+ if color is not None:
109
+ body["color"] = color
110
+ if tags is not None:
111
+ body["tags"] = tags
112
+ if visibility is not None:
113
+ body["visibility"] = visibility
114
+ if retrieval_config is not None:
115
+ body["retrieval_config"] = retrieval_config
116
+ if default_embedding_model is not None:
117
+ body["default_embedding_model"] = default_embedding_model
118
+ if default_chunking_strategy is not None:
119
+ body["default_chunking_strategy"] = default_chunking_strategy
120
+ if default_chunk_size is not None:
121
+ body["default_chunk_size"] = default_chunk_size
122
+ if default_chunk_overlap is not None:
123
+ body["default_chunk_overlap"] = default_chunk_overlap
124
+
125
+ data = self._http.patch(f"/v1/collections/{id}", body)
126
+ return Collection._from_dict(data["collection"])
127
+
128
+ def delete(self, id: str) -> None:
129
+ self._http.delete(f"/v1/collections/{id}")
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.5
2
+ Name: liyaengine
3
+ Version: 0.1.0
4
+ Summary: Official Python client for the Liya Engine public API
5
+ Project-URL: Homepage, https://liyaengine.ai
6
+ Project-URL: Documentation, https://liyaengine.ai/docs/sdks/python
7
+ Project-URL: Repository, https://github.com/liyaengine/sdk-python
8
+ Author-email: Liya Engine <support@liyaengine.ai>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,client,liyaengine,sdk
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: httpx<1,>=0.27
23
+ Provides-Extra: dev
24
+ Requires-Dist: mypy>=1.10; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: respx>=0.21; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # liyaengine
31
+
32
+ Official Python client for the [Liya Engine](https://liyaengine.ai) public API.
33
+
34
+ > **Status: early access.** This SDK currently covers the Collections resource. More resources (Domains, Run, Agents, Workflows, Evals) ship incrementally — see [Roadmap](#roadmap).
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install liyaengine
40
+ ```
41
+
42
+ ## Quickstart
43
+
44
+ ```python
45
+ from liyaengine import LiyaEngine
46
+
47
+ client = LiyaEngine(api_key="liya_...")
48
+
49
+ collection = client.collections.create(
50
+ slug="contracts",
51
+ label="Contracts",
52
+ domain_keys=["legal-ops"],
53
+ )
54
+
55
+ collections = client.collections.list()
56
+ ```
57
+
58
+ Or as a context manager (closes the underlying HTTP connection pool automatically):
59
+
60
+ ```python
61
+ with LiyaEngine(api_key="liya_...") as client:
62
+ collections = client.collections.list()
63
+ ```
64
+
65
+ Get an API key from your [Liya Engine dashboard](https://app.liyaengine.ai) under Settings → API Keys.
66
+
67
+ ## Error handling
68
+
69
+ Every failed request raises `LiyaEngineAPIError`, carrying the API's `code`, `message`, and HTTP `status`:
70
+
71
+ ```python
72
+ from liyaengine import LiyaEngineAPIError
73
+
74
+ try:
75
+ client.collections.create(slug="contracts", label="Contracts", domain_keys=["legal-ops"])
76
+ except LiyaEngineAPIError as err:
77
+ if err.code == "SLUG_CONFLICT":
78
+ # handle the conflict
79
+ pass
80
+ raise
81
+ ```
82
+
83
+ Network failures and timeouts raise `LiyaEngineNetworkError` instead. Requests are retried automatically on `429`/`5xx` responses and transient network errors (2 retries by default).
84
+
85
+ ## Configuration
86
+
87
+ ```python
88
+ LiyaEngine(
89
+ api_key="liya_...",
90
+ base_url="https://api.liyaengine.ai", # override for local/staging
91
+ timeout_s=30.0,
92
+ max_retries=2,
93
+ )
94
+ ```
95
+
96
+ ## Roadmap
97
+
98
+ - [x] Collections
99
+ - [ ] Domains (custom domain + intent CRUD)
100
+ - [ ] Run / Run (streaming)
101
+ - [ ] Agents
102
+ - [ ] Workflows
103
+ - [ ] Evaluations
104
+ - [ ] Async client
105
+
106
+ Full docs: https://liyaengine.ai/docs/sdks/python
107
+
108
+ ## Development
109
+
110
+ ```bash
111
+ python3 -m venv .venv && source .venv/bin/activate
112
+ pip install -e ".[dev]"
113
+ mypy src
114
+ pytest
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,11 @@
1
+ liyaengine/__init__.py,sha256=ZG04UwwHSSPSBBcwfcFT298C9LUZWKJowicv9DXw3DQ,270
2
+ liyaengine/_http.py,sha256=RuKGyJSZ5pVo5PLUaPCxOPKp_EEuH3_MgcrVxdiyXSQ,3431
3
+ liyaengine/client.py,sha256=zruIS37du-SGQLSy4VS9ZdFpkXAYd5w55opW_n1mtw4,1272
4
+ liyaengine/errors.py,sha256=2809-O2vM05FueoMfSlrm24kBBNpI7u10MR6bkd9g1E,1097
5
+ liyaengine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ liyaengine/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ liyaengine/resources/collections.py,sha256=2TkB4ToCkWFZO97buxliU_IUHj8XT69jdvhVzeAsqi0,4774
8
+ liyaengine-0.1.0.dist-info/METADATA,sha256=M0SEpdoSM0KF7w0hKcWOrB2ti361OqRX2KMbzBs8pnM,3157
9
+ liyaengine-0.1.0.dist-info/WHEEL,sha256=6zicbvgMWfHSD_sSydPAPL1UNY8-QHIQA4J8fOxyAc8,87
10
+ liyaengine-0.1.0.dist-info/licenses/LICENSE,sha256=C8CsX4Wo9A3N2HP3EiDgH1PLS4NBBiogtEuXVas6lMA,1067
11
+ liyaengine-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LiyaEngine
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.