afs-core 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.
- afs_core-0.1.0/.gitignore +41 -0
- afs_core-0.1.0/PKG-INFO +49 -0
- afs_core-0.1.0/README.md +28 -0
- afs_core-0.1.0/pyproject.toml +46 -0
- afs_core-0.1.0/src/afs_core/__init__.py +9 -0
- afs_core-0.1.0/src/afs_core/contracts/__init__.py +11 -0
- afs_core-0.1.0/src/afs_core/contracts/catalog.py +90 -0
- afs_core-0.1.0/src/afs_core/contracts/normalize.py +40 -0
- afs_core-0.1.0/src/afs_core/contracts/objects.py +44 -0
- afs_core-0.1.0/src/afs_core/errors.py +212 -0
- afs_core-0.1.0/src/afs_core/keys.py +271 -0
- afs_core-0.1.0/src/afs_core/models/__init__.py +43 -0
- afs_core-0.1.0/src/afs_core/models/control.py +62 -0
- afs_core-0.1.0/src/afs_core/models/core.py +60 -0
- afs_core-0.1.0/src/afs_core/models/extraction.py +50 -0
- afs_core-0.1.0/src/afs_core/models/objects.py +26 -0
- afs_core-0.1.0/src/afs_core/py.typed +0 -0
- afs_core-0.1.0/src/afs_core/testing/__init__.py +23 -0
- afs_core-0.1.0/src/afs_core/testing/conformance.py +220 -0
- afs_core-0.1.0/src/afs_core/testing/memory.py +285 -0
- afs_core-0.1.0/tests/test_conformance_memory.py +28 -0
- afs_core-0.1.0/tests/test_contracts.py +14 -0
- afs_core-0.1.0/tests/test_errors.py +47 -0
- afs_core-0.1.0/tests/test_keys.py +96 -0
- afs_core-0.1.0/tests/test_models.py +55 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# --- OS / editor ---
|
|
2
|
+
.DS_Store
|
|
3
|
+
Thumbs.db
|
|
4
|
+
*.swp
|
|
5
|
+
.idea/
|
|
6
|
+
.vscode/
|
|
7
|
+
|
|
8
|
+
# --- Python (packages land in M0+) ---
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
.uv/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.ty_cache/
|
|
19
|
+
dist/
|
|
20
|
+
build/
|
|
21
|
+
|
|
22
|
+
# --- Node (workers/mcp-edge lands later) ---
|
|
23
|
+
node_modules/
|
|
24
|
+
npm-debug.log*
|
|
25
|
+
|
|
26
|
+
# --- Secrets / local env ---
|
|
27
|
+
.env
|
|
28
|
+
.env.*
|
|
29
|
+
!.env.example
|
|
30
|
+
*.secret.*
|
|
31
|
+
|
|
32
|
+
# --- Terraform ---
|
|
33
|
+
# Detailed Terraform ignores live in terraform/.gitignore; these catch any
|
|
34
|
+
# stray state/plan artifacts produced outside that tree.
|
|
35
|
+
*.tfstate
|
|
36
|
+
*.tfstate.*
|
|
37
|
+
*.tfplan
|
|
38
|
+
.terraform/
|
|
39
|
+
|
|
40
|
+
# Agent worktrees (isolated background-agent checkouts)
|
|
41
|
+
.claude/worktrees/
|
afs_core-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: afs-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: agentic-fs core contracts, DTOs, key scheme, and conformance kits. Depends on pydantic only.
|
|
5
|
+
Project-URL: Homepage, https://github.com/vivekkhimani/agentic-fs
|
|
6
|
+
Project-URL: Repository, https://github.com/vivekkhimani/agentic-fs
|
|
7
|
+
Project-URL: Issues, https://github.com/vivekkhimani/agentic-fs/issues
|
|
8
|
+
Author-email: Vivek Khimani <vivekkhimani07@gmail.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: agentic-fs,agents,conformance,contracts,filesystem,mcp
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: pydantic>=2.7
|
|
17
|
+
Provides-Extra: testing
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'testing'
|
|
19
|
+
Requires-Dist: pytest>=8; extra == 'testing'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# afs-core
|
|
23
|
+
|
|
24
|
+
The **contracts** package for agentic-fs: `typing.Protocol` interfaces, pydantic
|
|
25
|
+
DTOs, the single key scheme, the closed error vocabulary, versioned event
|
|
26
|
+
contracts, and the conformance test kits. Depends on **pydantic only** — it
|
|
27
|
+
imports without the server, so connectors and adopters can build against it
|
|
28
|
+
without pulling in `afs-server`.
|
|
29
|
+
|
|
30
|
+
## Status
|
|
31
|
+
|
|
32
|
+
Foundations slice (M0, in progress):
|
|
33
|
+
|
|
34
|
+
- `afs_core.keys` — the **single** definition of the S3 key scheme: build, parse,
|
|
35
|
+
validate, and `is_indexable()`. Nothing else concatenates a key.
|
|
36
|
+
- `afs_core.errors` — the closed `ErrorCode` vocabulary + the `AfsError`
|
|
37
|
+
hierarchy (RFC 9457 `problem+json` shape).
|
|
38
|
+
- `afs_core.models` — core DTOs (`Page[T]`, `CatalogEntry`, `ExtractionState`, …)
|
|
39
|
+
and control records (`TenantRecord`, `NamespaceRecord`, `PrincipalRecord`).
|
|
40
|
+
|
|
41
|
+
Coming next: `afs_core.contracts` (the async Protocols), `afs_core.testing`
|
|
42
|
+
(conformance kits + in-memory fakes), `afs_core.events`.
|
|
43
|
+
|
|
44
|
+
## Develop
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv sync
|
|
48
|
+
uv run pytest packages/afs-core
|
|
49
|
+
```
|
afs_core-0.1.0/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# afs-core
|
|
2
|
+
|
|
3
|
+
The **contracts** package for agentic-fs: `typing.Protocol` interfaces, pydantic
|
|
4
|
+
DTOs, the single key scheme, the closed error vocabulary, versioned event
|
|
5
|
+
contracts, and the conformance test kits. Depends on **pydantic only** — it
|
|
6
|
+
imports without the server, so connectors and adopters can build against it
|
|
7
|
+
without pulling in `afs-server`.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
Foundations slice (M0, in progress):
|
|
12
|
+
|
|
13
|
+
- `afs_core.keys` — the **single** definition of the S3 key scheme: build, parse,
|
|
14
|
+
validate, and `is_indexable()`. Nothing else concatenates a key.
|
|
15
|
+
- `afs_core.errors` — the closed `ErrorCode` vocabulary + the `AfsError`
|
|
16
|
+
hierarchy (RFC 9457 `problem+json` shape).
|
|
17
|
+
- `afs_core.models` — core DTOs (`Page[T]`, `CatalogEntry`, `ExtractionState`, …)
|
|
18
|
+
and control records (`TenantRecord`, `NamespaceRecord`, `PrincipalRecord`).
|
|
19
|
+
|
|
20
|
+
Coming next: `afs_core.contracts` (the async Protocols), `afs_core.testing`
|
|
21
|
+
(conformance kits + in-memory fakes), `afs_core.events`.
|
|
22
|
+
|
|
23
|
+
## Develop
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv sync
|
|
27
|
+
uv run pytest packages/afs-core
|
|
28
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "afs-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "agentic-fs core contracts, DTOs, key scheme, and conformance kits. Depends on pydantic only."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
authors = [{ name = "Vivek Khimani", email = "vivekkhimani07@gmail.com" }]
|
|
9
|
+
keywords = [
|
|
10
|
+
"agentic-fs",
|
|
11
|
+
"agents",
|
|
12
|
+
"contracts",
|
|
13
|
+
"conformance",
|
|
14
|
+
"mcp",
|
|
15
|
+
"filesystem",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pydantic>=2.7",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
# The conformance kits + fakes in ``afs_core.testing`` import pytest at module
|
|
29
|
+
# level. Production adopters get just the contracts (``pip install afs-core``);
|
|
30
|
+
# anyone certifying a store/connector adds ``afs-core[testing]``.
|
|
31
|
+
testing = [
|
|
32
|
+
"pytest>=8",
|
|
33
|
+
"pytest-asyncio>=0.24",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/vivekkhimani/agentic-fs"
|
|
38
|
+
Repository = "https://github.com/vivekkhimani/agentic-fs"
|
|
39
|
+
Issues = "https://github.com/vivekkhimani/agentic-fs/issues"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/afs_core"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""agentic-fs core: contracts, DTOs, the key scheme, and the error vocabulary.
|
|
2
|
+
|
|
3
|
+
Depends on pydantic only — importable without the server. (``afs_core.testing``
|
|
4
|
+
is intentionally not imported here: it depends on pytest and is opt-in.)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from afs_core import contracts, errors, keys, models
|
|
8
|
+
|
|
9
|
+
__all__ = ["contracts", "errors", "keys", "models"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""The agentic-fs contracts: async ``typing.Protocol`` interfaces (plan §5).
|
|
2
|
+
|
|
3
|
+
Structural — adopters implement without importing our hierarchy. Each is proven
|
|
4
|
+
by a conformance kit in :mod:`afs_core.testing`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from afs_core.contracts.catalog import CatalogStore
|
|
8
|
+
from afs_core.contracts.normalize import NormalizationError, Normalizer
|
|
9
|
+
from afs_core.contracts.objects import ObjectStore
|
|
10
|
+
|
|
11
|
+
__all__ = ["CatalogStore", "NormalizationError", "Normalizer", "ObjectStore"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""The ``CatalogStore`` contract (plan §5.1).
|
|
2
|
+
|
|
3
|
+
One contract covers entries + control records + checkpoints + scratch quota, so a
|
|
4
|
+
self-hoster swaps **one** stateful dependency. Structural ``Protocol``, async.
|
|
5
|
+
Certify an impl with ``CatalogStoreConformance`` (afs_core.testing); DynamoDB and
|
|
6
|
+
Postgres are the two reference implementations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
from afs_core.models import (
|
|
14
|
+
CatalogEntry,
|
|
15
|
+
ExtractionState,
|
|
16
|
+
NamespaceRecord,
|
|
17
|
+
Page,
|
|
18
|
+
PrincipalRecord,
|
|
19
|
+
ScratchUsage,
|
|
20
|
+
SyncCheckpoint,
|
|
21
|
+
TenantRecord,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@runtime_checkable
|
|
26
|
+
class CatalogStore(Protocol):
|
|
27
|
+
# -- entries (derived index of S3; healable FROM S3) --
|
|
28
|
+
async def put_entry(self, entry: CatalogEntry) -> None: ...
|
|
29
|
+
|
|
30
|
+
async def get_entry(self, tenant_id: str, namespace: str, path: str) -> CatalogEntry | None: ...
|
|
31
|
+
|
|
32
|
+
async def delete_entry(
|
|
33
|
+
self, tenant_id: str, namespace: str, path: str, *, hard: bool = False
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Tombstone (soft) by default; ``hard=True`` removes the row entirely."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
async def list_entries(
|
|
39
|
+
self,
|
|
40
|
+
tenant_id: str,
|
|
41
|
+
namespace: str,
|
|
42
|
+
*,
|
|
43
|
+
prefix: str = "",
|
|
44
|
+
include_deleted: bool = False,
|
|
45
|
+
cursor: str | None = None,
|
|
46
|
+
limit: int = 1000,
|
|
47
|
+
) -> Page[CatalogEntry]: ...
|
|
48
|
+
|
|
49
|
+
async def find_by_checksum(self, tenant_id: str, checksum: str) -> list[CatalogEntry]: ...
|
|
50
|
+
|
|
51
|
+
async def set_extraction(
|
|
52
|
+
self, tenant_id: str, namespace: str, path: str, state: ExtractionState
|
|
53
|
+
) -> None: ...
|
|
54
|
+
|
|
55
|
+
async def list_by_extraction_status(
|
|
56
|
+
self, status: str, *, cursor: str | None = None, limit: int = 100
|
|
57
|
+
) -> Page[CatalogEntry]: ...
|
|
58
|
+
|
|
59
|
+
async def tree_version(self, tenant_id: str, namespace: str) -> str:
|
|
60
|
+
"""A token bumped on any write to the namespace — the tree-cache key."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
# -- control records (tenants / namespaces / principals) --
|
|
64
|
+
async def put_tenant(self, tenant: TenantRecord) -> None: ...
|
|
65
|
+
async def get_tenant(self, tenant_id: str) -> TenantRecord | None: ...
|
|
66
|
+
async def list_tenants(
|
|
67
|
+
self, *, cursor: str | None = None, limit: int = 100
|
|
68
|
+
) -> Page[TenantRecord]: ...
|
|
69
|
+
|
|
70
|
+
async def put_namespace(self, ns: NamespaceRecord) -> None: ...
|
|
71
|
+
async def get_namespace(self, tenant_id: str, name: str) -> NamespaceRecord | None: ...
|
|
72
|
+
async def list_namespaces(self, tenant_id: str) -> list[NamespaceRecord]: ...
|
|
73
|
+
async def delete_namespace(self, tenant_id: str, name: str) -> None: ...
|
|
74
|
+
|
|
75
|
+
async def put_principal(self, p: PrincipalRecord) -> None: ...
|
|
76
|
+
async def get_principal(self, tenant_id: str, principal_id: str) -> PrincipalRecord | None: ...
|
|
77
|
+
async def list_principals(self, tenant_id: str) -> list[PrincipalRecord]: ...
|
|
78
|
+
|
|
79
|
+
# -- connector checkpoints --
|
|
80
|
+
async def get_checkpoint(self, tenant_id: str, connector_id: str) -> SyncCheckpoint | None: ...
|
|
81
|
+
async def put_checkpoint(
|
|
82
|
+
self, tenant_id: str, connector_id: str, cp: SyncCheckpoint
|
|
83
|
+
) -> None: ...
|
|
84
|
+
|
|
85
|
+
# -- scratch quota (atomic; raises QuotaExceededError) --
|
|
86
|
+
async def adjust_scratch_usage(
|
|
87
|
+
self, tenant_id: str, principal_id: str, *, delta_bytes: int, delta_objects: int
|
|
88
|
+
) -> ScratchUsage: ...
|
|
89
|
+
|
|
90
|
+
async def get_scratch_usage(self, tenant_id: str, principal_id: str) -> ScratchUsage: ...
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""The ``Normalizer`` contract (plan §5.4) — the extractor/parser seam.
|
|
2
|
+
|
|
3
|
+
A normalizer turns one raw document into normalized per-page markdown. It is the
|
|
4
|
+
*only* place document parsing lives: text_native, docling, llamaparse, and any
|
|
5
|
+
custom parser are all just `Normalizer`s. The `ExtractionPipeline` (in
|
|
6
|
+
afs-server) orders them into a ladder, applies a quality gate, and degrades to
|
|
7
|
+
`catalog_only` — none of which a normalizer needs to know about.
|
|
8
|
+
|
|
9
|
+
Adding your own: implement this Protocol, certify it against
|
|
10
|
+
`afs_core.testing.NormalizerConformance`, register it via the `afs.normalizers`
|
|
11
|
+
entry-point group, and name it in the extraction ladder. No core changes.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
from afs_core.models import NormalizedDocument, SourceDocument
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NormalizationError(Exception):
|
|
22
|
+
"""A normalizer couldn't parse the document. Carries a closed-vocabulary
|
|
23
|
+
``reason`` (events v1) so the pipeline can record why and try the next rung."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, reason: str, message: str | None = None) -> None:
|
|
26
|
+
self.reason = reason
|
|
27
|
+
super().__init__(message or reason)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class Normalizer(Protocol):
|
|
32
|
+
name: str
|
|
33
|
+
|
|
34
|
+
def accepts(self, doc: SourceDocument) -> bool:
|
|
35
|
+
"""Whether this normalizer claims the document (by MIME/extension)."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
async def normalize(self, doc: SourceDocument) -> NormalizedDocument:
|
|
39
|
+
"""Parse ``doc`` into per-page markdown, or raise ``NormalizationError``."""
|
|
40
|
+
...
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""The ``ObjectStore`` contract (plan §5.2).
|
|
2
|
+
|
|
3
|
+
Structural ``Protocol`` — adopters implement it without importing our hierarchy
|
|
4
|
+
or depending on ``afs-server``. S3 is the only production impl; the protocol
|
|
5
|
+
exists so MinIO/LocalStack back local dev and an in-memory fake backs tests.
|
|
6
|
+
Certify any impl with ``ObjectStoreConformance`` (afs_core.testing).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
from afs_core.models import ObjectStat, Page, PresignedPut
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class ObjectStore(Protocol):
|
|
18
|
+
async def get(self, key: str, *, start: int | None = None, end: int | None = None) -> bytes:
|
|
19
|
+
"""Fetch an object, optionally a byte range ``[start, end]`` (inclusive)."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
async def put(
|
|
23
|
+
self, key: str, body: bytes, *, content_type: str | None = None
|
|
24
|
+
) -> ObjectStat: ...
|
|
25
|
+
|
|
26
|
+
async def delete(self, key: str) -> None: ...
|
|
27
|
+
|
|
28
|
+
async def delete_prefix(self, prefix: str) -> int:
|
|
29
|
+
"""Delete every object under ``prefix``; returns the count removed."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
async def stat(self, key: str) -> ObjectStat | None:
|
|
33
|
+
"""Metadata for ``key``, or ``None`` if it does not exist."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
async def list(
|
|
37
|
+
self, prefix: str, *, cursor: str | None = None, limit: int = 1000
|
|
38
|
+
) -> Page[ObjectStat]: ...
|
|
39
|
+
|
|
40
|
+
async def presigned_put(
|
|
41
|
+
self, key: str, *, content_type: str, max_bytes: int, expires_in: int = 900
|
|
42
|
+
) -> PresignedPut: ...
|
|
43
|
+
|
|
44
|
+
async def presigned_get(self, key: str, *, expires_in: int = 300) -> str: ...
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""The closed error vocabulary and the ``AfsError`` hierarchy.
|
|
2
|
+
|
|
3
|
+
Every error agentic-fs raises across the wire carries a code from the closed
|
|
4
|
+
:class:`ErrorCode` enum and serializes to an RFC 9457 ``application/problem+json``
|
|
5
|
+
envelope. The vocabulary is closed on purpose: clients (and the MCP tool layer)
|
|
6
|
+
can branch on a small, stable set of codes instead of parsing prose.
|
|
7
|
+
|
|
8
|
+
Design note — **misses are 404, never 403** (plan §4.1): a caller must not be
|
|
9
|
+
able to tell "exists but forbidden" from "does not exist", or they could
|
|
10
|
+
enumerate tenants/namespaces/documents. So the not-found errors below all map to
|
|
11
|
+
404 and there is intentionally no 403 for resource access.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ErrorCode(StrEnum):
|
|
21
|
+
"""Closed vocabulary of machine-readable error codes."""
|
|
22
|
+
|
|
23
|
+
# Request / validation
|
|
24
|
+
VALIDATION_ERROR = "validation_error"
|
|
25
|
+
INVALID_KEY = "invalid_key"
|
|
26
|
+
INVALID_NAMESPACE = "invalid_namespace"
|
|
27
|
+
|
|
28
|
+
# Not found (also used to hide forbidden — see module docstring)
|
|
29
|
+
NOT_FOUND = "not_found"
|
|
30
|
+
TENANT_NOT_FOUND = "tenant_not_found"
|
|
31
|
+
NAMESPACE_NOT_FOUND = "namespace_not_found"
|
|
32
|
+
DOCUMENT_NOT_FOUND = "document_not_found"
|
|
33
|
+
|
|
34
|
+
# AuthN / AuthZ
|
|
35
|
+
UNAUTHENTICATED = "unauthenticated"
|
|
36
|
+
INSUFFICIENT_SCOPE = "insufficient_scope"
|
|
37
|
+
|
|
38
|
+
# Read-path / capability
|
|
39
|
+
CATALOG_ONLY = "catalog_only"
|
|
40
|
+
SEARCH_NOT_ENABLED = "search_not_enabled"
|
|
41
|
+
BUDGET_EXCEEDED = "budget_exceeded"
|
|
42
|
+
|
|
43
|
+
# Write-path / quota
|
|
44
|
+
QUOTA_EXCEEDED = "quota_exceeded"
|
|
45
|
+
PAYLOAD_TOO_LARGE = "payload_too_large"
|
|
46
|
+
CONFLICT = "conflict"
|
|
47
|
+
|
|
48
|
+
# Extraction
|
|
49
|
+
EXTRACTION_FAILED = "extraction_failed"
|
|
50
|
+
|
|
51
|
+
# Catch-all
|
|
52
|
+
INTERNAL = "internal"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AfsError(Exception):
|
|
56
|
+
"""Base for every agentic-fs error.
|
|
57
|
+
|
|
58
|
+
Carries a closed :class:`ErrorCode`, an HTTP status, and an optional
|
|
59
|
+
``detail`` map; serializes to an RFC 9457 problem object via
|
|
60
|
+
:meth:`to_problem`.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
code: ErrorCode = ErrorCode.INTERNAL
|
|
64
|
+
http_status: int = 500
|
|
65
|
+
title: str = "Internal error"
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
message: str | None = None,
|
|
70
|
+
*,
|
|
71
|
+
detail: dict[str, Any] | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
self.message = message or self.title
|
|
74
|
+
self.detail = detail or {}
|
|
75
|
+
super().__init__(self.message)
|
|
76
|
+
|
|
77
|
+
def to_problem(self, *, instance: str | None = None) -> dict[str, Any]:
|
|
78
|
+
"""Render as an RFC 9457 ``application/problem+json`` object."""
|
|
79
|
+
problem: dict[str, Any] = {
|
|
80
|
+
"type": f"https://agentic-fs.dev/errors/{self.code.value}",
|
|
81
|
+
"title": self.title,
|
|
82
|
+
"status": self.http_status,
|
|
83
|
+
"code": self.code.value,
|
|
84
|
+
"detail": self.message,
|
|
85
|
+
}
|
|
86
|
+
if instance is not None:
|
|
87
|
+
problem["instance"] = instance
|
|
88
|
+
problem.update(self.detail)
|
|
89
|
+
return problem
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# --- 4xx: client ---------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ValidationError(AfsError):
|
|
96
|
+
code = ErrorCode.VALIDATION_ERROR
|
|
97
|
+
http_status = 400
|
|
98
|
+
title = "Validation error"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class InvalidKeyError(ValidationError):
|
|
102
|
+
code = ErrorCode.INVALID_KEY
|
|
103
|
+
title = "Invalid object key"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class InvalidNamespaceError(ValidationError):
|
|
107
|
+
code = ErrorCode.INVALID_NAMESPACE
|
|
108
|
+
title = "Invalid namespace"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class UnauthenticatedError(AfsError):
|
|
112
|
+
code = ErrorCode.UNAUTHENTICATED
|
|
113
|
+
http_status = 401
|
|
114
|
+
title = "Unauthenticated"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class InsufficientScopeError(AfsError):
|
|
118
|
+
code = ErrorCode.INSUFFICIENT_SCOPE
|
|
119
|
+
http_status = 403
|
|
120
|
+
title = "Insufficient scope"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class NotFoundError(AfsError):
|
|
124
|
+
"""Generic 404. Also the disguise for forbidden resource access (§4.1)."""
|
|
125
|
+
|
|
126
|
+
code = ErrorCode.NOT_FOUND
|
|
127
|
+
http_status = 404
|
|
128
|
+
title = "Not found"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TenantNotFoundError(NotFoundError):
|
|
132
|
+
code = ErrorCode.TENANT_NOT_FOUND
|
|
133
|
+
title = "Tenant not found"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class NamespaceNotFoundError(NotFoundError):
|
|
137
|
+
code = ErrorCode.NAMESPACE_NOT_FOUND
|
|
138
|
+
title = "Namespace not found"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class DocumentNotFoundError(NotFoundError):
|
|
142
|
+
code = ErrorCode.DOCUMENT_NOT_FOUND
|
|
143
|
+
title = "Document not found"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ConflictError(AfsError):
|
|
147
|
+
code = ErrorCode.CONFLICT
|
|
148
|
+
http_status = 409
|
|
149
|
+
title = "Conflict"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PayloadTooLargeError(AfsError):
|
|
153
|
+
code = ErrorCode.PAYLOAD_TOO_LARGE
|
|
154
|
+
http_status = 413
|
|
155
|
+
title = "Payload too large"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class QuotaExceededError(AfsError):
|
|
159
|
+
code = ErrorCode.QUOTA_EXCEEDED
|
|
160
|
+
http_status = 429
|
|
161
|
+
title = "Quota exceeded"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class BudgetExceededError(AfsError):
|
|
165
|
+
code = ErrorCode.BUDGET_EXCEEDED
|
|
166
|
+
http_status = 422
|
|
167
|
+
title = "Budget exceeded"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class CatalogOnlyError(AfsError):
|
|
171
|
+
"""The document exists and is cite-able, but its contents aren't readable yet."""
|
|
172
|
+
|
|
173
|
+
code = ErrorCode.CATALOG_ONLY
|
|
174
|
+
http_status = 422
|
|
175
|
+
title = "Document is catalog-only"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class SearchNotEnabledError(AfsError):
|
|
179
|
+
code = ErrorCode.SEARCH_NOT_ENABLED
|
|
180
|
+
http_status = 422
|
|
181
|
+
title = "Search is not enabled"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# --- 5xx: server ---------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ExtractionFailedError(AfsError):
|
|
188
|
+
code = ErrorCode.EXTRACTION_FAILED
|
|
189
|
+
http_status = 500
|
|
190
|
+
title = "Extraction failed"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
__all__ = [
|
|
194
|
+
"AfsError",
|
|
195
|
+
"BudgetExceededError",
|
|
196
|
+
"CatalogOnlyError",
|
|
197
|
+
"ConflictError",
|
|
198
|
+
"DocumentNotFoundError",
|
|
199
|
+
"ErrorCode",
|
|
200
|
+
"ExtractionFailedError",
|
|
201
|
+
"InsufficientScopeError",
|
|
202
|
+
"InvalidKeyError",
|
|
203
|
+
"InvalidNamespaceError",
|
|
204
|
+
"NamespaceNotFoundError",
|
|
205
|
+
"NotFoundError",
|
|
206
|
+
"PayloadTooLargeError",
|
|
207
|
+
"QuotaExceededError",
|
|
208
|
+
"SearchNotEnabledError",
|
|
209
|
+
"TenantNotFoundError",
|
|
210
|
+
"UnauthenticatedError",
|
|
211
|
+
"ValidationError",
|
|
212
|
+
]
|