tileward 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.
Files changed (46) hide show
  1. tileward-0.1.0/.github/workflows/ci.yml +28 -0
  2. tileward-0.1.0/.github/workflows/publish.yml +44 -0
  3. tileward-0.1.0/.gitignore +17 -0
  4. tileward-0.1.0/LICENSE +21 -0
  5. tileward-0.1.0/PKG-INFO +348 -0
  6. tileward-0.1.0/README.md +290 -0
  7. tileward-0.1.0/docs/device-auth.md +99 -0
  8. tileward-0.1.0/pyproject.toml +87 -0
  9. tileward-0.1.0/src/tileward/__init__.py +55 -0
  10. tileward-0.1.0/src/tileward/_http.py +365 -0
  11. tileward-0.1.0/src/tileward/_mcp.py +251 -0
  12. tileward-0.1.0/src/tileward/_version.py +1 -0
  13. tileward-0.1.0/src/tileward/auth.py +200 -0
  14. tileward-0.1.0/src/tileward/cli/__init__.py +1 -0
  15. tileward-0.1.0/src/tileward/cli/commands/__init__.py +1 -0
  16. tileward-0.1.0/src/tileward/cli/commands/account.py +95 -0
  17. tileward-0.1.0/src/tileward/cli/commands/auth.py +181 -0
  18. tileward-0.1.0/src/tileward/cli/commands/chat.py +174 -0
  19. tileward-0.1.0/src/tileward/cli/commands/config.py +103 -0
  20. tileward-0.1.0/src/tileward/cli/commands/context.py +239 -0
  21. tileward-0.1.0/src/tileward/cli/commands/docs.py +166 -0
  22. tileward-0.1.0/src/tileward/cli/commands/guard.py +107 -0
  23. tileward-0.1.0/src/tileward/cli/commands/keys.py +143 -0
  24. tileward-0.1.0/src/tileward/cli/commands/models.py +53 -0
  25. tileward-0.1.0/src/tileward/cli/main.py +263 -0
  26. tileward-0.1.0/src/tileward/cli/output.py +134 -0
  27. tileward-0.1.0/src/tileward/client.py +215 -0
  28. tileward-0.1.0/src/tileward/config.py +243 -0
  29. tileward-0.1.0/src/tileward/errors.py +129 -0
  30. tileward-0.1.0/src/tileward/py.typed +0 -0
  31. tileward-0.1.0/src/tileward/resources/__init__.py +1 -0
  32. tileward-0.1.0/src/tileward/resources/account.py +67 -0
  33. tileward-0.1.0/src/tileward/resources/chat.py +258 -0
  34. tileward-0.1.0/src/tileward/resources/context.py +273 -0
  35. tileward-0.1.0/src/tileward/resources/documents.py +180 -0
  36. tileward-0.1.0/src/tileward/resources/guard.py +91 -0
  37. tileward-0.1.0/src/tileward/resources/keys.py +102 -0
  38. tileward-0.1.0/src/tileward/resources/models.py +106 -0
  39. tileward-0.1.0/tests/conftest.py +41 -0
  40. tileward-0.1.0/tests/test_cli.py +205 -0
  41. tileward-0.1.0/tests/test_config.py +69 -0
  42. tileward-0.1.0/tests/test_device_auth.py +264 -0
  43. tileward-0.1.0/tests/test_errors.py +64 -0
  44. tileward-0.1.0/tests/test_mcp.py +120 -0
  45. tileward-0.1.0/tests/test_resources.py +289 -0
  46. tileward-0.1.0/tests/test_transport.py +171 -0
@@ -0,0 +1,28 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ # The floor and the ceiling of what pyproject claims. A package that says 3.9+ and is only
15
+ # ever run on 3.12 is claiming something nobody checks.
16
+ python-version: ["3.9", "3.13"]
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - run: pip install -e ".[dev]"
23
+ - run: pytest -q
24
+ - run: ruff check src tests
25
+ - name: mypy
26
+ # The type check targets one version; running it twice would report the same result twice.
27
+ if: matrix.python-version == '3.13'
28
+ run: mypy src/tileward
@@ -0,0 +1,44 @@
1
+ name: publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ # NO API TOKEN. This uses PyPI Trusted Publishing (OIDC): PyPI is configured to trust this
9
+ # repository and this workflow filename, and mints a short-lived credential per run. Nothing
10
+ # long-lived is stored in the repo, in Actions secrets, or on anyone's laptop.
11
+ #
12
+ # Setup on PyPI, once: Publishing -> add a pending publisher for project `tileward`,
13
+ # owner `Tileward-com`, repository `tileward-cli`, workflow `publish.yml`.
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ test:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.11"
25
+ - run: pip install -e ".[dev]"
26
+ - run: pytest -q
27
+
28
+ publish:
29
+ # Never publish a red build. A version number cannot be reused on PyPI, so a broken upload is
30
+ # permanent -- the only fix is a new version, and the bad one stays installable.
31
+ needs: test
32
+ runs-on: ubuntu-latest
33
+ permissions:
34
+ id-token: write
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: actions/setup-python@v5
38
+ with:
39
+ python-version: "3.11"
40
+ - run: pip install build
41
+ - run: python -m build
42
+ - name: Check the long description renders on PyPI
43
+ run: pip install twine && twine check dist/*
44
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .coverage
13
+ htmlcov/
14
+ .DS_Store
15
+ _scratch/
16
+ _scratch/
17
+ _scratch/
tileward-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tileward
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.
@@ -0,0 +1,348 @@
1
+ Metadata-Version: 2.5
2
+ Name: tileward
3
+ Version: 0.1.0
4
+ Summary: Tileward client library and CLI (twcli): run models, govern them, and work with context and documents.
5
+ Project-URL: Homepage, https://tileward.com
6
+ Project-URL: Documentation, https://tileward.com/docs
7
+ Project-URL: Source, https://github.com/Tileward-com/tileward-cli
8
+ Project-URL: Issues, https://github.com/Tileward-com/tileward-cli/issues
9
+ Project-URL: Changelog, https://github.com/Tileward-com/tileward-cli/releases
10
+ Project-URL: Dashboard, https://app.tileward.com/account
11
+ Author: Tileward
12
+ License: MIT License
13
+
14
+ Copyright (c) 2026 Tileward
15
+
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ of this software and associated documentation files (the "Software"), to deal
18
+ in the Software without restriction, including without limitation the rights
19
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
+ copies of the Software, and to permit persons to whom the Software is
21
+ furnished to do so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in all
24
+ copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
+ SOFTWARE.
33
+ License-File: LICENSE
34
+ Keywords: cli,context,governance,llm,mcp,openai-compatible,tileward
35
+ Classifier: Development Status :: 4 - Beta
36
+ Classifier: Environment :: Console
37
+ Classifier: Intended Audience :: Developers
38
+ Classifier: License :: OSI Approved :: MIT License
39
+ Classifier: Programming Language :: Python :: 3
40
+ Classifier: Programming Language :: Python :: 3.9
41
+ Classifier: Programming Language :: Python :: 3.10
42
+ Classifier: Programming Language :: Python :: 3.11
43
+ Classifier: Programming Language :: Python :: 3.12
44
+ Classifier: Programming Language :: Python :: 3.13
45
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
46
+ Classifier: Typing :: Typed
47
+ Requires-Python: >=3.9
48
+ Requires-Dist: click>=8.0
49
+ Requires-Dist: httpx<1,>=0.24
50
+ Requires-Dist: rich>=13.0
51
+ Provides-Extra: dev
52
+ Requires-Dist: mypy>=1.8; extra == 'dev'
53
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
54
+ Requires-Dist: pytest>=7.4; extra == 'dev'
55
+ Requires-Dist: respx>=0.20; extra == 'dev'
56
+ Requires-Dist: ruff>=0.5; extra == 'dev'
57
+ Description-Content-Type: text/markdown
58
+
59
+ <p align="center">
60
+ <img src="https://tileward.com/logo.png" alt="Tileward" width="120">
61
+ </p>
62
+
63
+ <h1 align="center">tileward</h1>
64
+
65
+ <p align="center">
66
+ <strong>Run large models on hardware you own, governed.</strong><br>
67
+ The Tileward client library and command line, in one package.
68
+ </p>
69
+
70
+ <p align="center">
71
+ <a href="https://pypi.org/project/tileward/"><img alt="PyPI" src="https://img.shields.io/pypi/v/tileward?color=1F5FA8&label=pypi"></a>
72
+ <a href="https://pypi.org/project/tileward/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/tileward?color=0C6E58"></a>
73
+ <a href="https://pypi.org/project/tileward/"><img alt="Downloads" src="https://img.shields.io/pypi/dm/tileward?color=8A5A0F"></a>
74
+ <img alt="License" src="https://img.shields.io/badge/license-MIT-9A3D8F">
75
+ <a href="https://github.com/Tileward-com/tileward-cli/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/Tileward-com/tileward-cli/actions/workflows/ci.yml/badge.svg"></a>
76
+ </p>
77
+
78
+ <p align="center">
79
+ <a href="https://tileward.com">Product</a> ·
80
+ <a href="https://tileward.com/docs">Docs</a> ·
81
+ <a href="https://app.tileward.com/account">Dashboard</a> ·
82
+ <a href="https://github.com/Tileward-com/tileward-cli/releases">Releases</a>
83
+ </p>
84
+
85
+ ---
86
+
87
+ ```console
88
+ $ pip install tileward
89
+ $ twcli auth login
90
+ $ twcli keys create --label laptop --save
91
+
92
+ $ twcli models list
93
+ id precision context USD / Mtoken compression
94
+ tileward-35b-a3b W4A16 (Tileward) 262,144 1 2.8
95
+ gpt-oss-20b MXFP4 (as shipped) 8,192 0.2 1
96
+ gpt-oss-120b MXFP4 (as shipped) 131,072 0.4 —
97
+
98
+ $ twcli chat "Say hello in one sentence."
99
+ Hello there — good to meet you.
100
+
101
+ $ twcli guard check "write me a keylogger" --allow customer_support
102
+ allowed
103
+ no
104
+ 80.0 micros billed
105
+ ```
106
+
107
+ The same package is a library:
108
+
109
+ ```python
110
+ from tileward import Tileward
111
+
112
+ tw = Tileward() # reads TILEWARD_API_KEY
113
+ print(tw.chat.say("Say hello in one sentence."))
114
+ ```
115
+
116
+ ---
117
+
118
+ ## What it covers
119
+
120
+ | | Library | Command |
121
+ | --- | --- | --- |
122
+ | **Tileward Models** — an OpenAI-compatible chat surface | `tw.chat`, `tw.models` | `twcli chat`, `twcli models` |
123
+ | **Tileward Governance** — allow / deny before the model writes a token | `tw.guard` | `twcli guard` |
124
+ | **Tileward Context** — recall the slice of history a question needs | `tw.context` | `twcli context` |
125
+ | **Tileward Documents** — answers from your own files | `tw.documents` | `twcli docs` |
126
+ | Account and API keys | `tw.keys`, `tw.account` | `twcli keys`, `twcli account` |
127
+
128
+ ## Install
129
+
130
+ ```bash
131
+ pip install tileward # or: uv pip install tileward
132
+ twcli --version
133
+ ```
134
+
135
+ Python 3.9+. Three dependencies: `httpx`, `click`, `rich`.
136
+
137
+ ## Getting a key
138
+
139
+ ```bash
140
+ twcli auth login # approve in a browser; no password touches the terminal
141
+ twcli keys create --label laptop --save
142
+ ```
143
+
144
+ `auth login` runs a device-code flow ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)): the CLI
145
+ shows a short code and a URL, you approve it in a browser, and the terminal receives a console
146
+ session. `keys create` then mints an API key.
147
+
148
+ Two credentials, and they are not interchangeable:
149
+
150
+ - an **API key** (`tw_live_…`) calls the model, the guard, Context and Documents;
151
+ - a **console session** manages the account — minting keys, reading billing, binding policies.
152
+
153
+ A key that could mint keys would survive its own revocation, so it cannot. If you already have a
154
+ key, skip the login:
155
+
156
+ ```bash
157
+ export TILEWARD_API_KEY="tw_live_..."
158
+ ```
159
+
160
+ ## The command line
161
+
162
+ ```
163
+ twcli auth login|logout|status sign in, and see what this machine holds
164
+ twcli models list|show what is served right now
165
+ twcli chat [PROMPT] [-i] one prompt, a pipe, or a REPL
166
+ twcli guard check TEXT [--allow ...] allow / deny, no generation
167
+ twcli context recall|remember|pin|forget|topics|stats|threads|reset|purge
168
+ twcli docs ls|add|rm|search your own files
169
+ twcli keys list|create|rotate|revoke mint and retire API keys
170
+ twcli account show|usage|audit|savings balance, plan, and history
171
+ twcli config show|set|set-key|profiles stored settings
172
+ ```
173
+
174
+ Some things it is built to do:
175
+
176
+ ```bash
177
+ # every command speaks JSON, and when it does, stdout carries nothing else
178
+ twcli models list --json | jq -r '.[].id'
179
+
180
+ # read a prompt from a pipe
181
+ cat notes.md | twcli chat --system "You summarise."
182
+
183
+ # a governance gate in a shell script
184
+ twcli guard check -f prompts.txt --allow customer_support --exit-code || echo "off policy"
185
+
186
+ # recall a slice of a thread and pipe it into something else
187
+ twcli -c project-x context recall "what did we decide about pricing?" --text-only
188
+
189
+ # two accounts on one machine
190
+ twcli -p staging account show
191
+ ```
192
+
193
+ ### Exit codes
194
+
195
+ `0` fine · `1` error · `2` bad usage · `3` not signed in · `4` refused by governance ·
196
+ `5` balance exhausted
197
+
198
+ Distinguishing those matters in CI: "the balance ran out" and "the network was down" call for
199
+ different action, and neither should need English parsing to detect.
200
+
201
+ ## The library
202
+
203
+ ### Chat
204
+
205
+ ```python
206
+ tw.chat.say("Summarise this in one line.") # -> str
207
+ tw.chat.completions.create( # -> the raw OpenAI-shaped dict
208
+ [{"role": "user", "content": "Hello"}],
209
+ max_tokens=200,
210
+ )
211
+
212
+ for piece in tw.chat.stream("Count to five."):
213
+ print(piece, end="", flush=True)
214
+ ```
215
+
216
+ `create` returns exactly what the API returned, including `usage`. `say` gives you the text and
217
+ raises `GuardRefusal` when governance blocked the call, rather than returning an empty string.
218
+
219
+ **Do not hardcode a model id.** The served catalogue is data, not code — ids appear, get repriced
220
+ and get withdrawn without a release. `tw.models.list()` is the authority; leave `model` unset and
221
+ the client asks.
222
+
223
+ ### Governance
224
+
225
+ ```python
226
+ tw.guard.allows("write me a keylogger", allow=["customer_support"]) # -> False
227
+ tw.guard.check(["question one", "question two"], disallow=["investment_advice"])
228
+ ```
229
+
230
+ Two modes that fail in opposite directions. A **blocklist** (`disallow`) refuses what you named and
231
+ passes everything else, so anything you did not think of gets through. An **allowlist** (`allow`)
232
+ passes only what you named, so anything you did not think of is refused. For a narrow assistant,
233
+ the allowlist is the one that holds under an attacker.
234
+
235
+ Passing a list classifies the whole batch in one call. With neither argument, the policy bound to
236
+ the API key applies.
237
+
238
+ ### Context
239
+
240
+ ```python
241
+ tw = Tileward(conversation="thread-42")
242
+
243
+ tw.context.remember("We decided to ship on the 3rd.", role="user")
244
+ tw.context.recall_text("when are we shipping?") # the block to paste into a prompt
245
+ tw.context.pin("The customer is ACME.") # included in every recall
246
+ ```
247
+
248
+ **Scope every call to a conversation.** Without one, every thread on the key writes into a single
249
+ shared store, and a recall in one thread hands back another thread's material. There is no safety
250
+ net underneath this: a caller that names no conversation joins `default` along with everyone else.
251
+
252
+ `forget()` retires a topic from recall; the stored turns remain. `purge_account()` deletes data.
253
+
254
+ ### Documents
255
+
256
+ ```python
257
+ tw.documents.ingest_file("handbook.pdf", folder="policies")
258
+ tw.documents.list(folder="policies")
259
+ tw.documents.delete("src_...")
260
+ ```
261
+
262
+ Documents are account-scoped, not conversation-scoped: a file you upload is available to every
263
+ thread on the key. That is why none of these take a `conversation`.
264
+
265
+ Deleting a document drops its chunks, but material already folded into a conversation's summary
266
+ can still surface in a recall. If something has to be genuinely gone, `context.purge_account()` is
267
+ the operation that means it.
268
+
269
+ ### Async
270
+
271
+ Every method has an awaitable twin on `AsyncTileward`, with the same names and arguments.
272
+
273
+ ```python
274
+ from tileward import AsyncTileward
275
+
276
+ async with AsyncTileward() as tw:
277
+ print(await tw.chat.say("Hello"))
278
+ async for piece in tw.chat.stream("Count to five."):
279
+ print(piece, end="")
280
+ ```
281
+
282
+ ### Errors
283
+
284
+ ```python
285
+ from tileward import errors
286
+
287
+ try:
288
+ tw.chat.say("...")
289
+ except errors.InsufficientBalanceError: # 402 — top up; calls resume immediately
290
+ ...
291
+ except errors.GuardRefusal as exc: # governance blocked it; exc.completion has the detail
292
+ ...
293
+ except errors.APIError as exc: # exc.status, exc.code, exc.request_id
294
+ ...
295
+ ```
296
+
297
+ A governed refusal is **not** an HTTP error. It arrives as an ordinary completion with
298
+ `finish_reason: "content_filter"` and zero tokens billed — you are not charged for a refusal —
299
+ which is why `create` passes it through and only `say` raises.
300
+
301
+ ## Configuration
302
+
303
+ Highest wins: an explicit argument, then the environment, then the profile, then the default.
304
+
305
+ | Environment | What it sets |
306
+ | --- | --- |
307
+ | `TILEWARD_API_KEY` | the key used for models, guard, Context and Documents |
308
+ | `TILEWARD_SESSION` | a console session, for the account surface |
309
+ | `TILEWARD_BASE_URL` | the API host — serves `/v1` only |
310
+ | `TILEWARD_CONSOLE_URL` | the console host — the account surface and `auth login` live here |
311
+ | `TILEWARD_CONTEXT_URL` | the Context host |
312
+ | `TILEWARD_CONVERSATION` | default conversation scope |
313
+ | `TILEWARD_MODEL` | default model id |
314
+ | `TILEWARD_PROFILE` | which stored profile to read |
315
+ | `TILEWARD_CONFIG_DIR` | where the files live (default `~/.config/tileward`) |
316
+
317
+ Settings go in `config.json`; credentials go in `credentials.json`, written `0600` in a `0700`
318
+ directory. They are separate files so showing your config never has to redact, and pasting it into
319
+ an issue has not pasted a key.
320
+
321
+ In a server process, `Tileward(load_config=False)` ignores the files entirely, so a developer's
322
+ `~/.config` cannot change how production behaves.
323
+
324
+ ## Using it from another framework
325
+
326
+ The chat surface is OpenAI-compatible, so anything that speaks OpenAI works by changing two things
327
+ — the base URL and the key.
328
+
329
+ ```python
330
+ from openai import OpenAI
331
+ from tileward import openai_base_url
332
+
333
+ client = OpenAI(base_url=openai_base_url(), api_key="tw_live_...")
334
+ ```
335
+
336
+ That helper exists so nobody has to guess whether `/v1` belongs on the end.
337
+
338
+ ## Links
339
+
340
+ - **Product** — [tileward.com](https://tileward.com)
341
+ - **Dashboard and keys** — [app.tileward.com/account](https://app.tileward.com/account)
342
+ - **What we ship and how each number was measured** — [tileward.com/llms.txt](https://tileward.com/llms.txt)
343
+ - **How `auth login` works** — [docs/device-auth.md](https://github.com/Tileward-com/tileward-cli/blob/main/docs/device-auth.md)
344
+ - **Issues** — [github.com/Tileward-com/tileward-cli/issues](https://github.com/Tileward-com/tileward-cli/issues)
345
+
346
+ ## License
347
+
348
+ MIT — see [LICENSE](https://github.com/Tileward-com/tileward-cli/blob/main/LICENSE).