Metadata-Version: 2.5
Name: artefaktum
Version: 0.3.0
Summary: Python client and CLI for Artefaktum: store, find and trust the artifacts that agents hand to each other.
Project-URL: Homepage, https://artefaktum.dev
Project-URL: Source, https://github.com/artefaktum-dev/artefaktum-python
Project-URL: Issues, https://github.com/artefaktum-dev/artefaktum-python/issues
License-Expression: MIT
License-File: LICENSE
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Provides-Extra: cli
Requires-Dist: click>=8.1; extra == 'cli'
Provides-Extra: dev
Requires-Dist: click>=8.1; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# artefaktum

Python client and CLI for [Artefaktum](https://artefaktum.dev): store, find and trust the
artifacts that AI agents hand to each other.

## Install

```bash
pip install artefaktum          # the client
pip install "artefaktum[cli]"   # plus the `artefaktum` command
```

Python 3.10+. The only dependency is `httpx`.

## Quick start

Create an API key in the [console](https://artefaktum.dev/console/) and export it as
`ARTEFAKTUM_API_KEY`.

```python
from artefaktum import Artefaktum

client = Artefaktum()
artifact = client.artifacts.push("report.pdf", title="Q3 churn", tags=["churn"])
hits = client.artifacts.search("q3 churn")
client.artifacts.pull(hits.items[0].artifact.id, "out/")
```

## Configuration

Explicit argument, then environment variable, then default.

| Argument | Environment variable | Default |
|---|---|---|
| `api_key` | `ARTEFAKTUM_API_KEY` | required |
| `base_url` | `ARTEFAKTUM_BASE_URL` | `https://api.artefaktum.dev` |
| `project` | `ARTEFAKTUM_PROJECT` | `default` |

`project` is a slug or a UUID. Project-scoped methods take a per-call `project=` override.

## Uploading

```python
client.artifacts.push("data/export.parquet", title="Export")  # a path, streamed
client.artifacts.push(payload, title="Export", filename="export.json")  # bytes
client.artifacts.create_version(artifact.id, "data/export-v2.parquet")
```

`push` hashes the source, uploads it straight to object storage (the API key is never sent
there) and waits until the artifact is `ready`. `wait=False` returns as soon as the upload
is accepted.

### Compute once, reuse everywhere

```python
r = client.artifacts.resolve(
    "openweather/vilnius/2026-09-21",
    filename="weather.json",
    content_type="application/json",
    size_bytes=len(body),
    title="Vilnius weather",
    max_age=3600,
)
if r.status == "hit":
    use(r.artifact)
elif r.status == "create":
    client.artifacts.fulfil(r, body)
else:
    ...  # "pending": another producer is already making it
```

### Reuse or replace by key

```python
from datetime import timedelta

# Reuse: an artifact under this key younger than an hour is returned, nothing is uploaded.
a = client.artifacts.push(
    body,
    filename="weather.json",
    title="Vilnius weather",
    external_key="openweather/vilnius",
    if_exists="reuse",
    max_age=timedelta(hours=1),
)
print(a.reused)  # True when an existing artifact came back (set by the SDK, not the API)

# Replace: add a version to the artifact holding the key, or create it when the key is new.
client.artifacts.push(
    body,
    filename="weather.json",
    title="Vilnius weather",
    external_key="openweather/vilnius",
    if_exists="version",
)

# Read it back by key; a `max_age` older artifact is NotFound.
client.artifacts.pull_by_key("openweather/vilnius", "out/", max_age=timedelta(hours=1))
```

`if_exists` is `"error"` (the default: an existing key is a `Conflict`), `"version"` or
`"reuse"`; both need `external_key`, and `max_age` only applies to `"reuse"`. With `"reuse"`,
a `push` that finds another producer already making the artifact waits up to `timeout`
seconds, then raises `ProcessingTimeout`.

## Downloading

```python
client.artifacts.pull(artifact.id, "out/")  # into a directory, server-side filename
client.artifacts.pull(artifact.id, "out/report.pdf")  # to an exact path
```

`pull` verifies the sha256 digest and raises `IntegrityError` on a mismatch. A string
ending in `/` is a directory, created if needed.

## Finding

```python
client.artifacts.search("emission factors", mode="hybrid", tags_all=["ghg"])
client.artifacts.get_by_external_key("reports/q3-churn")
for a in client.artifacts.iter_all(tag="churn"):
    print(a.title)
```

Search hides superseded artifacts by default; each hit says whether it is `superseded` or
`stale_upstream`.

## Everything else

`client.artifacts`: `get`, `list`, `update`, `delete`, `versions`, `relations`,
`add_relation`, `download_url`, `create_upload`, `complete_upload`. `client.runs`:
`create`, `seal`, `artifacts`. `client.projects.list()`. `client.keys` and `client.usage`
(admin scope). `client.whoami()`, `client.quota()`.

## Async

`AsyncArtefaktum` mirrors every method as a coroutine; `iter_all` becomes an async
generator.

```python
import asyncio
from artefaktum import AsyncArtefaktum


async def main():
    async with AsyncArtefaktum() as client:
        await client.artifacts.push("report.pdf", title="Q3 churn")
        async for a in client.artifacts.iter_all(tag="churn"):
            print(a.id)


asyncio.run(main())
```

## Command line

```bash
artefaktum login                          # prompts for the key, saves it with mode 0600
artefaktum push report.pdf --title "Q3 churn" --tag churn
artefaktum search "q3 churn"
artefaktum pull <artifact-id> out/
artefaktum get --key vendor-x/report/2026-09 --max-age 1h
artefaktum pull --key vendor-x/report/2026-09 out/
artefaktum push report.pdf --title "Q3 churn" --key q3-churn --if-exists reuse --max-age 1d
```

`push --if-exists` is `error` (default), `version` (add a version to the artifact holding
`--key`) or `reuse` (return that artifact, uploading only if there is none or it is older
than `--max-age`). Under `reuse` the output gains `reused`, and `--wait SECONDS` (default 60)
is, with `reuse`, the seconds to wait on another writer and then again for the artifact to
become ready (ignored otherwise; `--timeout` is ignored under `reuse`). `pull --key K` takes its destination as
`--dest`.

`afk` is a shorter alias. Output is a table on a terminal and JSON when piped; `--json` /
`--table` force either. The key comes from `--api-key`, then `ARTEFAKTUM_API_KEY`, then the
saved config. Exit codes: `0` ok, `1` API error, `2` usage error, `3` not found, `4`
`push --if-exists reuse` still pending after `--wait`, `130` interrupted.

Commands: `push`, `pull`, `get`, `ls`, `search`, `relate`, `relations`,
`versions`, `link`, `rm`, `projects`, `whoami`, `login`, `logout`. `artefaktum --help`
has the details.

## Errors

Every failure is an `ArtefaktumError` with `code`, `status` and `request_id` (quote it when
contacting support). Subclasses: `NotFound`, `Unauthorized`, `Forbidden`, `QuotaExceeded`,
`RequestTooLarge`, `Conflict`, `ValidationFailed`, `UploadError`, `ServiceUnavailable`, `StorageError`,
`ProcessingFailed`, `ProcessingTimeout`, `IntegrityError`. `MissingApiKey` is a
`ValueError` raised before any request is made.

```python
from artefaktum import NotFound

try:
    client.artifacts.get(artifact_id)
except NotFound as e:
    print(e.code, e.request_id)
```

`QuotaExceeded` means a plan limit is reached: 413 for storage or file size, 429 for the
month's API calls. `client.quota()` reports the plan, its limits and current usage.

`RequestTooLarge` (413, code `request_too_large`) means the JSON request itself is over
256 KB, almost always because of oversized metadata. It has nothing to do with the size
of your file. Field limits are listed at <https://artefaktum.dev/docs/rest/#limits>.

Reads are retried up to 3 attempts on 429, 502, 503, 504 and connection errors, honouring
`Retry-After`. Writes and storage transfers are never retried.

## Issues and contributions

Open an issue for bugs and questions. Pull requests are welcome; they are applied upstream
and land here with the next release rather than being merged directly.

## License

MIT
