praixis 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,17 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(ls \"C:\\\\Users\\\\meramirez\\\\Desktop\\\\PythonProjects\\\\PraixisEngine\")",
5
+ "Read(//c/Users/meramirez/Desktop/PythonProjects/**)",
6
+ "Bash(uv run *)",
7
+ "Bash(PRAIXIS_KEY='praixis_HZU7igk-i6LPxzPJNPv6V-3iA3O8qDBprRbkZOoTKog' PRAIXIS_URL='http://172.30.1.144:8080' uv run --no-project python _live_check.py)",
8
+ "Bash(rm -f praixis/resources/admin.py praixis/aio/resources/admin.py)",
9
+ "Bash(rm -f praixis/resources/__pycache__/admin*.pyc praixis/aio/resources/__pycache__/admin*.pyc)",
10
+ "Bash(PRAIXIS_KEY=praixis_HZU7igk-i6LPxzPJNPv6V-3iA3O8qDBprRbkZOoTKog uv run --no-project python -c ' *)",
11
+ "Bash(New-Item -ItemType File praixis/py.typed)",
12
+ "Bash(Out-Null)",
13
+ "Bash(Test-Path praixis/py.typed)",
14
+ "Bash(uv build *)"
15
+ ]
16
+ }
17
+ }
@@ -0,0 +1,21 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .mypy_cache/
11
+ .pytest_cache/
12
+ uv.lock
13
+
14
+ # Editor / OS
15
+ .vscode/
16
+ .idea/
17
+ .DS_Store
18
+ Thumbs.db
19
+
20
+ # Internal notes (not for distribution)
21
+ NOTES.md
praixis-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michael E. Ramirez A.
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.
praixis-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: praixis
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency Python client for the Praixis Engine API
5
+ Project-URL: Homepage, https://github.com/mettjs/praixis-python
6
+ Project-URL: Repository, https://github.com/mettjs/praixis-python
7
+ Project-URL: Issues, https://github.com/mettjs/praixis-python/issues
8
+ Author: Michael E. Ramirez A.
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,llm,praixis,rag
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: async
25
+ Requires-Dist: httpx>=0.24; extra == 'async'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Praixis Engine — Python Client
29
+
30
+ A lightweight Python client for the Praixis Engine API, in both **sync** and
31
+ **async** flavors.
32
+
33
+ - **`PraixisClient`** — synchronous, **zero dependencies**, built entirely on
34
+ the standard library (`urllib`, `json`, `uuid`), so an upstream package
35
+ release can never break it.
36
+ - **`AsyncPraixisClient`** — async/await, built on `httpx` (the only optional
37
+ dependency). Imported lazily, so the sync client stays dependency-free.
38
+ - Same surface in both: resource-grouped `client.chat` and `client.rag` — chat
39
+ + file summary, and RAG ingest/ask/embed/compare.
40
+
41
+ > The companion Node.js client lives in its own repository.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install praixis # sync client only, zero dependencies
47
+ pip install "praixis[async]" # also installs httpx for the async client
48
+ ```
49
+
50
+ Or vendor the `praixis/` package directly into your project — the sync client
51
+ has no deps.
52
+
53
+ Requires Python 3.10+.
54
+
55
+ ## Authentication
56
+
57
+ Every endpoint uses an app-level API key sent as the `X-API-Key` header.
58
+
59
+ ```python
60
+ from praixis import PraixisClient
61
+
62
+ client = PraixisClient("http://localhost:8080", "your-api-key")
63
+ ```
64
+
65
+ > Admin/system routes (`/api/system/*`, HTTP Basic auth) are intentionally **not**
66
+ > part of this SDK. They already have a browser UI, and baking admin credentials
67
+ > into application code would be a security anti-pattern. The `X-API-Key` this
68
+ > SDK uses is an app-level credential, not an admin one.
69
+
70
+ ## Chat
71
+
72
+ ```python
73
+ # Start a conversation
74
+ reply = client.chat.send("Hello, world!")
75
+ print(reply["session_id"], reply["response"])
76
+
77
+ # Continue it
78
+ client.chat.send("And again?", session_id=reply["session_id"])
79
+
80
+ # JSON-mode response, custom system prompt
81
+ client.chat.send("List 3 colors", response_format="json", system_prompt="Be terse")
82
+
83
+ # Sessions
84
+ client.chat.list_sessions() # -> [session_id, ...]
85
+ client.chat.get_history(session_id) # -> {"session_id", "history": [...]}
86
+ client.chat.clear_history(session_id)
87
+
88
+ # Summarize an uploaded file (path, or (filename, content[, content_type]))
89
+ client.chat.summarize_file("report.pdf")
90
+ client.chat.summarize_file(("notes.txt", "raw text here"))
91
+ ```
92
+
93
+ > **Note on streaming:** the server streams chat and RAG answers as
94
+ > `text/event-stream`, not JSON. This client buffers the full response and
95
+ > parses the leading marker lines (`[SESSION_ID:...]`, and for RAG
96
+ > `[SEARCH_QUERY:...]` / `[SOURCES:...]`) out of the body for you, so
97
+ > `chat.send` returns `{"session_id", "response", "response_format"}` and
98
+ > `rag.ask` returns `{"answer", "sources", "session_id", "search_query"}`.
99
+ > Buffering is the right default for scripts and backends; token-by-token
100
+ > iteration is not yet exposed.
101
+
102
+ ## RAG
103
+
104
+ ```python
105
+ # Ingest one or many documents into a collection
106
+ client.rag.upload("manual.pdf", collection_name="docs")
107
+ client.rag.upload([("a.txt", "..."), ("b.txt", "...")], collection_name="docs")
108
+
109
+ # Ask a question grounded in a collection
110
+ ans = client.rag.ask("What does the manual say about setup?", collection_name="docs")
111
+ print(ans)
112
+
113
+ # Embeddings, listing, deletion, compare, summarize
114
+ client.rag.embed("some text")
115
+ client.rag.list_collections()
116
+ client.rag.list_files("docs")
117
+ client.rag.delete_file("docs", "a.txt")
118
+ client.rag.delete_collection("docs")
119
+ client.rag.compare("docs", "a.txt", "b.txt")
120
+ client.rag.summarize_document("docs", "manual.pdf")
121
+ ```
122
+
123
+ ## Error handling
124
+
125
+ ```python
126
+ from praixis import (
127
+ APIError, AuthenticationError, NotFoundError,
128
+ RateLimitError, APIConnectionError,
129
+ )
130
+
131
+ try:
132
+ client.chat.send("hi")
133
+ except AuthenticationError:
134
+ ... # 401 / 403
135
+ except NotFoundError:
136
+ ... # 404
137
+ except RateLimitError:
138
+ ... # 429 (per-route limits)
139
+ except APIError as e:
140
+ print(e.status_code, e.detail)
141
+ except APIConnectionError:
142
+ ... # never reached the server
143
+ ```
144
+
145
+ All exceptions inherit from `praixis.PraixisError`.
146
+
147
+ ## Testing
148
+
149
+ Both suites run against a standard-library mock HTTP server — no network needed.
150
+
151
+ ```bash
152
+ # Sync client — zero dependencies
153
+ uv run --no-project python tests/test_client.py
154
+
155
+ # Async client — needs httpx
156
+ uv run --with httpx python tests/test_async_client.py
157
+ ```
158
+
159
+ The async suite also asserts that the sync and async resources expose an
160
+ identical set of methods, so the two clients can't silently drift apart.
161
+
162
+ ## Privacy note
163
+
164
+ This client transmits whatever you pass to it (prompts, documents, session IDs)
165
+ to the configured Praixis Engine server. Those payloads may contain personal
166
+ data — handle them according to your own privacy obligations. The client stores
167
+ nothing locally and adds no telemetry.
168
+
169
+ ## License
170
+
171
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,144 @@
1
+ # Praixis Engine — Python Client
2
+
3
+ A lightweight Python client for the Praixis Engine API, in both **sync** and
4
+ **async** flavors.
5
+
6
+ - **`PraixisClient`** — synchronous, **zero dependencies**, built entirely on
7
+ the standard library (`urllib`, `json`, `uuid`), so an upstream package
8
+ release can never break it.
9
+ - **`AsyncPraixisClient`** — async/await, built on `httpx` (the only optional
10
+ dependency). Imported lazily, so the sync client stays dependency-free.
11
+ - Same surface in both: resource-grouped `client.chat` and `client.rag` — chat
12
+ + file summary, and RAG ingest/ask/embed/compare.
13
+
14
+ > The companion Node.js client lives in its own repository.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install praixis # sync client only, zero dependencies
20
+ pip install "praixis[async]" # also installs httpx for the async client
21
+ ```
22
+
23
+ Or vendor the `praixis/` package directly into your project — the sync client
24
+ has no deps.
25
+
26
+ Requires Python 3.10+.
27
+
28
+ ## Authentication
29
+
30
+ Every endpoint uses an app-level API key sent as the `X-API-Key` header.
31
+
32
+ ```python
33
+ from praixis import PraixisClient
34
+
35
+ client = PraixisClient("http://localhost:8080", "your-api-key")
36
+ ```
37
+
38
+ > Admin/system routes (`/api/system/*`, HTTP Basic auth) are intentionally **not**
39
+ > part of this SDK. They already have a browser UI, and baking admin credentials
40
+ > into application code would be a security anti-pattern. The `X-API-Key` this
41
+ > SDK uses is an app-level credential, not an admin one.
42
+
43
+ ## Chat
44
+
45
+ ```python
46
+ # Start a conversation
47
+ reply = client.chat.send("Hello, world!")
48
+ print(reply["session_id"], reply["response"])
49
+
50
+ # Continue it
51
+ client.chat.send("And again?", session_id=reply["session_id"])
52
+
53
+ # JSON-mode response, custom system prompt
54
+ client.chat.send("List 3 colors", response_format="json", system_prompt="Be terse")
55
+
56
+ # Sessions
57
+ client.chat.list_sessions() # -> [session_id, ...]
58
+ client.chat.get_history(session_id) # -> {"session_id", "history": [...]}
59
+ client.chat.clear_history(session_id)
60
+
61
+ # Summarize an uploaded file (path, or (filename, content[, content_type]))
62
+ client.chat.summarize_file("report.pdf")
63
+ client.chat.summarize_file(("notes.txt", "raw text here"))
64
+ ```
65
+
66
+ > **Note on streaming:** the server streams chat and RAG answers as
67
+ > `text/event-stream`, not JSON. This client buffers the full response and
68
+ > parses the leading marker lines (`[SESSION_ID:...]`, and for RAG
69
+ > `[SEARCH_QUERY:...]` / `[SOURCES:...]`) out of the body for you, so
70
+ > `chat.send` returns `{"session_id", "response", "response_format"}` and
71
+ > `rag.ask` returns `{"answer", "sources", "session_id", "search_query"}`.
72
+ > Buffering is the right default for scripts and backends; token-by-token
73
+ > iteration is not yet exposed.
74
+
75
+ ## RAG
76
+
77
+ ```python
78
+ # Ingest one or many documents into a collection
79
+ client.rag.upload("manual.pdf", collection_name="docs")
80
+ client.rag.upload([("a.txt", "..."), ("b.txt", "...")], collection_name="docs")
81
+
82
+ # Ask a question grounded in a collection
83
+ ans = client.rag.ask("What does the manual say about setup?", collection_name="docs")
84
+ print(ans)
85
+
86
+ # Embeddings, listing, deletion, compare, summarize
87
+ client.rag.embed("some text")
88
+ client.rag.list_collections()
89
+ client.rag.list_files("docs")
90
+ client.rag.delete_file("docs", "a.txt")
91
+ client.rag.delete_collection("docs")
92
+ client.rag.compare("docs", "a.txt", "b.txt")
93
+ client.rag.summarize_document("docs", "manual.pdf")
94
+ ```
95
+
96
+ ## Error handling
97
+
98
+ ```python
99
+ from praixis import (
100
+ APIError, AuthenticationError, NotFoundError,
101
+ RateLimitError, APIConnectionError,
102
+ )
103
+
104
+ try:
105
+ client.chat.send("hi")
106
+ except AuthenticationError:
107
+ ... # 401 / 403
108
+ except NotFoundError:
109
+ ... # 404
110
+ except RateLimitError:
111
+ ... # 429 (per-route limits)
112
+ except APIError as e:
113
+ print(e.status_code, e.detail)
114
+ except APIConnectionError:
115
+ ... # never reached the server
116
+ ```
117
+
118
+ All exceptions inherit from `praixis.PraixisError`.
119
+
120
+ ## Testing
121
+
122
+ Both suites run against a standard-library mock HTTP server — no network needed.
123
+
124
+ ```bash
125
+ # Sync client — zero dependencies
126
+ uv run --no-project python tests/test_client.py
127
+
128
+ # Async client — needs httpx
129
+ uv run --with httpx python tests/test_async_client.py
130
+ ```
131
+
132
+ The async suite also asserts that the sync and async resources expose an
133
+ identical set of methods, so the two clients can't silently drift apart.
134
+
135
+ ## Privacy note
136
+
137
+ This client transmits whatever you pass to it (prompts, documents, session IDs)
138
+ to the configured Praixis Engine server. Those payloads may contain personal
139
+ data — handle them according to your own privacy obligations. The client stores
140
+ nothing locally and adds no telemetry.
141
+
142
+ ## License
143
+
144
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,63 @@
1
+ """Praixis Engine Python client.
2
+
3
+ The synchronous :class:`PraixisClient` is built entirely on the standard
4
+ library, so it has zero dependencies and upstream package releases can never
5
+ break it::
6
+
7
+ from praixis import PraixisClient
8
+
9
+ client = PraixisClient("http://localhost:8080", "your-api-key")
10
+ print(client.chat.send("Hello")["response"])
11
+
12
+ An :class:`AsyncPraixisClient` is also available for async/await code. It is
13
+ backed by httpx, the SDK's only (optional) dependency - install it with
14
+ ``pip install praixis[async]``. The import is lazy, so httpx is required only if
15
+ you actually use the async client; ``import praixis`` alone never needs it::
16
+
17
+ from praixis import AsyncPraixisClient
18
+
19
+ async with AsyncPraixisClient("http://localhost:8080", "your-api-key") as client:
20
+ print((await client.chat.send("Hello"))["response"])
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import TYPE_CHECKING
26
+
27
+ from .client import PraixisClient
28
+ from .errors import (
29
+ APIConnectionError,
30
+ APIError,
31
+ AuthenticationError,
32
+ NotFoundError,
33
+ PraixisError,
34
+ RateLimitError,
35
+ )
36
+
37
+ if TYPE_CHECKING:
38
+ # Give type checkers / IDEs the symbol without importing httpx at runtime.
39
+ from .aio import AsyncPraixisClient
40
+
41
+ __version__ = "0.1.0"
42
+
43
+ __all__ = [
44
+ "PraixisClient",
45
+ "AsyncPraixisClient",
46
+ "PraixisError",
47
+ "APIError",
48
+ "APIConnectionError",
49
+ "AuthenticationError",
50
+ "NotFoundError",
51
+ "RateLimitError",
52
+ "__version__",
53
+ ]
54
+
55
+
56
+ def __getattr__(name: str) -> object:
57
+ # Lazily resolve the async client so importing praixis never pulls in httpx
58
+ # unless the async client is actually requested.
59
+ if name == "AsyncPraixisClient":
60
+ from .aio import AsyncPraixisClient
61
+
62
+ return AsyncPraixisClient
63
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,45 @@
1
+ """Helpers for turning caller-supplied files into multipart parts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Union
7
+
8
+ from ._transport import FilePart
9
+
10
+ # What a caller may hand us for a single file:
11
+ # * a path on disk (str)
12
+ # * a (filename, content) pair
13
+ # * a (filename, content, content_type) triple
14
+ FileInput = Union[str, tuple]
15
+
16
+ _DEFAULT_CONTENT_TYPE = "application/octet-stream"
17
+
18
+
19
+ def to_part(item: FileInput, field_name: str) -> FilePart:
20
+ """Normalize one file input into a ``(field, filename, bytes, type)`` part."""
21
+ if isinstance(item, str):
22
+ with open(item, "rb") as fh:
23
+ content = fh.read()
24
+ return (field_name, os.path.basename(item), content, _DEFAULT_CONTENT_TYPE)
25
+
26
+ if isinstance(item, tuple):
27
+ if len(item) == 2:
28
+ filename, content = item
29
+ content_type = _DEFAULT_CONTENT_TYPE
30
+ elif len(item) == 3:
31
+ filename, content, content_type = item
32
+ else:
33
+ raise ValueError("file tuple must be (filename, content) or (filename, content, content_type)")
34
+ if isinstance(content, str):
35
+ content = content.encode("utf-8")
36
+ return (field_name, filename, content, content_type)
37
+
38
+ raise TypeError(f"unsupported file input: {type(item)!r}")
39
+
40
+
41
+ def to_parts(items: FileInput | list[FileInput], field_name: str) -> list[FilePart]:
42
+ """Normalize one file or a list of files into parts under ``field_name``."""
43
+ if isinstance(items, list):
44
+ return [to_part(it, field_name) for it in items]
45
+ return [to_part(items, field_name)]
@@ -0,0 +1,77 @@
1
+ """HTTP primitives shared by the sync and async transports.
2
+
3
+ Keeping these in one place means the auth scheme and the request/response
4
+ shapes are defined once and reused, so the two transports can never drift
5
+ apart.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Any
12
+ from urllib.parse import quote
13
+
14
+
15
+ def encode_path_segment(value: str) -> str:
16
+ """Percent-encode a single URL path segment (filename, session id, ...).
17
+
18
+ Uses ``safe=""`` so reserved characters - spaces, ``/``, ``?``, ``#``, ``%``
19
+ - in a value like a filename can't corrupt the URL. Values already limited to
20
+ ``[A-Za-z0-9_.-]`` (collection names, the usual ids) pass through unchanged.
21
+ """
22
+ return quote(str(value), safe="")
23
+
24
+ # A single file part: (field_name, filename, content_bytes, content_type).
25
+ FilePart = tuple[str, str, bytes, str]
26
+ # A single form field: (field_name, value).
27
+ FormField = tuple[str, str]
28
+
29
+
30
+ def auth_headers(api_key: str) -> dict[str, str]:
31
+ """Build the auth headers for a request.
32
+
33
+ Every app endpoint (chat + RAG) authenticates with the app-level
34
+ ``X-API-Key`` header. (Admin routes use HTTP Basic and a browser UI; they're
35
+ deliberately out of scope for this SDK.)
36
+ """
37
+ return {"X-API-Key": api_key} if api_key else {}
38
+
39
+
40
+ # A streamed (text/event-stream) line of the form ``[KEY:value]``. The server
41
+ # prefixes its streamed chat / RAG / summary responses with these marker lines
42
+ # (e.g. [SESSION_ID:...], [SEARCH_QUERY:...], [SOURCES:a,b], [FILE:...],
43
+ # [PROGRESS:...], [ERROR:...]) before emitting the generated text.
44
+ _MARKER_RE = re.compile(r"^\[([A-Z_]+):(.*)\]$")
45
+
46
+
47
+ def parse_event_stream(text: str) -> dict[str, Any]:
48
+ """Parse a buffered ``text/event-stream`` body into markers + generated text.
49
+
50
+ The chat, RAG-ask and file-summary endpoints don't return JSON - they stream
51
+ plain text whose leading lines are ``[KEY:value]`` markers followed by the
52
+ model's output. Both transports buffer that body and hand it here so the two
53
+ clients shape streamed responses identically.
54
+
55
+ Returns a dict with the recognised markers (``session_id``, ``search_query``,
56
+ ``sources`` as a list, ``file``), the joined generated ``text``, and the raw
57
+ ``markers`` map for anything else (e.g. ``PROGRESS``/``ERROR``).
58
+ """
59
+ markers: dict[str, str] = {}
60
+ body_lines: list[str] = []
61
+ for line in text.split("\n"):
62
+ m = _MARKER_RE.match(line)
63
+ if m:
64
+ markers[m.group(1)] = m.group(2)
65
+ else:
66
+ body_lines.append(line)
67
+
68
+ sources_raw = markers.get("SOURCES")
69
+ sources = [s for s in sources_raw.split(",") if s] if sources_raw is not None else None
70
+ return {
71
+ "session_id": markers.get("SESSION_ID"),
72
+ "search_query": markers.get("SEARCH_QUERY"),
73
+ "sources": sources,
74
+ "file": markers.get("FILE"),
75
+ "text": "\n".join(body_lines).strip("\n"),
76
+ "markers": markers,
77
+ }