semblance-foundry 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 (33) hide show
  1. semblance_foundry-0.1.0/LICENSE.md +21 -0
  2. semblance_foundry-0.1.0/PKG-INFO +138 -0
  3. semblance_foundry-0.1.0/README.md +105 -0
  4. semblance_foundry-0.1.0/pyproject.toml +57 -0
  5. semblance_foundry-0.1.0/setup.cfg +4 -0
  6. semblance_foundry-0.1.0/src/semblance_foundry/__init__.py +17 -0
  7. semblance_foundry-0.1.0/src/semblance_foundry/app.py +89 -0
  8. semblance_foundry-0.1.0/src/semblance_foundry/auth.py +167 -0
  9. semblance_foundry-0.1.0/src/semblance_foundry/cli.py +105 -0
  10. semblance_foundry-0.1.0/src/semblance_foundry/compatibility.py +30 -0
  11. semblance_foundry-0.1.0/src/semblance_foundry/compatibility.yaml +102 -0
  12. semblance_foundry-0.1.0/src/semblance_foundry/config.py +43 -0
  13. semblance_foundry-0.1.0/src/semblance_foundry/errors.py +131 -0
  14. semblance_foundry-0.1.0/src/semblance_foundry/fixtures/__init__.py +15 -0
  15. semblance_foundry-0.1.0/src/semblance_foundry/fixtures/defaults/__init__.py +0 -0
  16. semblance_foundry-0.1.0/src/semblance_foundry/fixtures/defaults/acme.yaml +80 -0
  17. semblance_foundry-0.1.0/src/semblance_foundry/fixtures/loaders.py +239 -0
  18. semblance_foundry-0.1.0/src/semblance_foundry/ids.py +89 -0
  19. semblance_foundry-0.1.0/src/semblance_foundry/models/__init__.py +92 -0
  20. semblance_foundry-0.1.0/src/semblance_foundry/pagination.py +43 -0
  21. semblance_foundry-0.1.0/src/semblance_foundry/py.typed +0 -0
  22. semblance_foundry-0.1.0/src/semblance_foundry/registry.py +29 -0
  23. semblance_foundry-0.1.0/src/semblance_foundry/services/__init__.py +5 -0
  24. semblance_foundry-0.1.0/src/semblance_foundry/services/ontologies/__init__.py +5 -0
  25. semblance_foundry-0.1.0/src/semblance_foundry/services/ontologies/routes.py +551 -0
  26. semblance_foundry-0.1.0/src/semblance_foundry/state.py +281 -0
  27. semblance_foundry-0.1.0/src/semblance_foundry/testing.py +66 -0
  28. semblance_foundry-0.1.0/src/semblance_foundry.egg-info/PKG-INFO +138 -0
  29. semblance_foundry-0.1.0/src/semblance_foundry.egg-info/SOURCES.txt +31 -0
  30. semblance_foundry-0.1.0/src/semblance_foundry.egg-info/dependency_links.txt +1 -0
  31. semblance_foundry-0.1.0/src/semblance_foundry.egg-info/entry_points.txt +2 -0
  32. semblance_foundry-0.1.0/src/semblance_foundry.egg-info/requires.txt +13 -0
  33. semblance_foundry-0.1.0/src/semblance_foundry.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Semblance Contributors
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 WITHOUT LIMITATION 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,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: semblance-foundry
3
+ Version: 0.1.0
4
+ Summary: Unofficial local HTTP simulation of selected Palantir Foundry API v2 ontology operations
5
+ Author: Semblance Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://semblance.readthedocs.io/en/latest/guides/foundry/
8
+ Project-URL: Repository, https://github.com/eddiethedean/semblance
9
+ Project-URL: Changelog, https://github.com/eddiethedean/semblance/blob/main/packages/semblance-foundry/CHANGELOG.md
10
+ Keywords: foundry,palantir,mock,simulation,testing,fastapi
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE.md
21
+ Requires-Dist: semblance<0.8,>=0.6.1
22
+ Requires-Dist: pydantic>=2.0.0
23
+ Requires-Dist: pyyaml>=6.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
26
+ Requires-Dist: httpx>=0.25.0; extra == "dev"
27
+ Requires-Dist: ruff>=0.16.0; extra == "dev"
28
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
29
+ Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
30
+ Provides-Extra: sdk
31
+ Requires-Dist: foundry-platform-sdk==1.101.0; extra == "sdk"
32
+ Dynamic: license-file
33
+
34
+ # semblance-foundry
35
+
36
+ **Unofficial** local HTTP simulation of selected [Palantir Foundry API v2](https://www.palantir.com/docs/foundry/api/v2) ontology-read operations.
37
+
38
+ This package is **not affiliated with, endorsed by, or equivalent to Palantir**. It does not implement Foundry's ontology engine, permissions, or compute. It is a test double for client development and CI.
39
+
40
+ Verified against public API v2 documentation dated **2026-08-18**. See `compatibility.yaml` (also served at `/.well-known/foundry-mock-compatibility.json`).
41
+
42
+ Requires `semblance` 0.6.1 or 0.7.x.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install semblance-foundry
48
+ ```
49
+
50
+ From a clone of this repository:
51
+
52
+ ```bash
53
+ pip install -e ".[dev]"
54
+ pip install -e "packages/semblance-foundry[dev]"
55
+ ```
56
+
57
+ Optional official SDK extra (not installed in default CI):
58
+
59
+ ```bash
60
+ pip install "semblance-foundry[sdk]"
61
+ pytest packages/semblance-foundry/tests -m sdk
62
+ ```
63
+
64
+ Pinned SDK: `foundry-platform-sdk==1.101.0`. Untested SDK versions are not implied compatible.
65
+
66
+ ## Quick start (HTTP)
67
+
68
+ ```python
69
+ from semblance_foundry import FoundryMock, FoundryMockConfig
70
+ from semblance_foundry.testing import foundry_test_client
71
+
72
+ foundry = FoundryMock(FoundryMockConfig(seed=42, auth="optional"))
73
+ foundry.load_bundled_fixture()
74
+ client = foundry_test_client(foundry)
75
+
76
+ r = client.get("/api/v2/ontologies/acme/objects/Employee?pageSize=2")
77
+ assert r.status_code == 200
78
+ ```
79
+
80
+ CLI:
81
+
82
+ ```bash
83
+ semblance-foundry fixture init --output foundry.yaml
84
+ semblance-foundry validate foundry.yaml
85
+ semblance-foundry operations
86
+ semblance-foundry serve --fixture foundry.yaml --port 8765
87
+ # or: semblance-foundry serve # bundled acme fixture
88
+ ```
89
+
90
+ Environment variables for local clients (not Palantir credentials): `SEMBLANCE_FOUNDRY_HOST`, `SEMBLANCE_FOUNDRY_TOKEN`.
91
+
92
+ ## Auth modes
93
+
94
+ - `disabled` — ignore `Authorization`
95
+ - `optional` (default) — missing token is OK; malformed `Bearer` values return 401
96
+ - `strict` — token must be in `FoundryMockConfig.tokens`
97
+
98
+ Authentication errors never echo the token. This is not an OAuth server and does not validate real Palantir credentials.
99
+
100
+ ## Fixtures
101
+
102
+ YAML/JSON version 1. Extra fields are rejected. Fixtures are **data only** — no expressions are evaluated. Register query callbacks in Python:
103
+
104
+ ```python
105
+ foundry.register_query("employeesByOffice", lambda params, state: {"data": []})
106
+ ```
107
+
108
+ `load_bundled_fixture()` loads the shipped `acme` example (one ontology, Employee + Office, `worksAt` links, `renameEmployee` action metadata, and `employeesByOffice` static query result). Use `load_fixture(path)` for your own files.
109
+
110
+ ## Pagination
111
+
112
+ List endpoints use `pageSize` / `pageToken` / `nextPageToken`. Tokens are opaque and checksummed. Tampering or cross-resource reuse returns `InvalidPageToken` (400). Default page size 100, max 1000.
113
+
114
+ ## Known unsupported (0.1.0)
115
+
116
+ - Action apply / applyBatch — `501 UnsupportedOperation` only when `auth=strict`; otherwise 404
117
+ - Aggregates, object sets, datasets, transactions, orchestration, streams
118
+ - Search filters other than `eq` and `and` of `eq` (400 `InvalidQuery`)
119
+
120
+ Unknown paths return Foundry-style 404.
121
+
122
+ ## Compatibility matrix (MVP)
123
+
124
+ | Level | Operations |
125
+ |---|---|
126
+ | exact | list/get ontologies, object types, objects, linked objects, action types (metadata), query types |
127
+ | representative | object search (`eq`/`and`), query execute (static or callback) |
128
+ | unsupported | apply, applyBatch |
129
+
130
+ ## Security
131
+
132
+ - Do not put production secrets in fixtures or tokens used with this mock
133
+ - Tokens are compared in-process and never written to error bodies
134
+ - Query/action behavior must be allow-listed Python, never YAML eval
135
+
136
+ ## License
137
+
138
+ MIT. Palantir and Foundry are trademarks of their respective owners.
@@ -0,0 +1,105 @@
1
+ # semblance-foundry
2
+
3
+ **Unofficial** local HTTP simulation of selected [Palantir Foundry API v2](https://www.palantir.com/docs/foundry/api/v2) ontology-read operations.
4
+
5
+ This package is **not affiliated with, endorsed by, or equivalent to Palantir**. It does not implement Foundry's ontology engine, permissions, or compute. It is a test double for client development and CI.
6
+
7
+ Verified against public API v2 documentation dated **2026-08-18**. See `compatibility.yaml` (also served at `/.well-known/foundry-mock-compatibility.json`).
8
+
9
+ Requires `semblance` 0.6.1 or 0.7.x.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install semblance-foundry
15
+ ```
16
+
17
+ From a clone of this repository:
18
+
19
+ ```bash
20
+ pip install -e ".[dev]"
21
+ pip install -e "packages/semblance-foundry[dev]"
22
+ ```
23
+
24
+ Optional official SDK extra (not installed in default CI):
25
+
26
+ ```bash
27
+ pip install "semblance-foundry[sdk]"
28
+ pytest packages/semblance-foundry/tests -m sdk
29
+ ```
30
+
31
+ Pinned SDK: `foundry-platform-sdk==1.101.0`. Untested SDK versions are not implied compatible.
32
+
33
+ ## Quick start (HTTP)
34
+
35
+ ```python
36
+ from semblance_foundry import FoundryMock, FoundryMockConfig
37
+ from semblance_foundry.testing import foundry_test_client
38
+
39
+ foundry = FoundryMock(FoundryMockConfig(seed=42, auth="optional"))
40
+ foundry.load_bundled_fixture()
41
+ client = foundry_test_client(foundry)
42
+
43
+ r = client.get("/api/v2/ontologies/acme/objects/Employee?pageSize=2")
44
+ assert r.status_code == 200
45
+ ```
46
+
47
+ CLI:
48
+
49
+ ```bash
50
+ semblance-foundry fixture init --output foundry.yaml
51
+ semblance-foundry validate foundry.yaml
52
+ semblance-foundry operations
53
+ semblance-foundry serve --fixture foundry.yaml --port 8765
54
+ # or: semblance-foundry serve # bundled acme fixture
55
+ ```
56
+
57
+ Environment variables for local clients (not Palantir credentials): `SEMBLANCE_FOUNDRY_HOST`, `SEMBLANCE_FOUNDRY_TOKEN`.
58
+
59
+ ## Auth modes
60
+
61
+ - `disabled` — ignore `Authorization`
62
+ - `optional` (default) — missing token is OK; malformed `Bearer` values return 401
63
+ - `strict` — token must be in `FoundryMockConfig.tokens`
64
+
65
+ Authentication errors never echo the token. This is not an OAuth server and does not validate real Palantir credentials.
66
+
67
+ ## Fixtures
68
+
69
+ YAML/JSON version 1. Extra fields are rejected. Fixtures are **data only** — no expressions are evaluated. Register query callbacks in Python:
70
+
71
+ ```python
72
+ foundry.register_query("employeesByOffice", lambda params, state: {"data": []})
73
+ ```
74
+
75
+ `load_bundled_fixture()` loads the shipped `acme` example (one ontology, Employee + Office, `worksAt` links, `renameEmployee` action metadata, and `employeesByOffice` static query result). Use `load_fixture(path)` for your own files.
76
+
77
+ ## Pagination
78
+
79
+ List endpoints use `pageSize` / `pageToken` / `nextPageToken`. Tokens are opaque and checksummed. Tampering or cross-resource reuse returns `InvalidPageToken` (400). Default page size 100, max 1000.
80
+
81
+ ## Known unsupported (0.1.0)
82
+
83
+ - Action apply / applyBatch — `501 UnsupportedOperation` only when `auth=strict`; otherwise 404
84
+ - Aggregates, object sets, datasets, transactions, orchestration, streams
85
+ - Search filters other than `eq` and `and` of `eq` (400 `InvalidQuery`)
86
+
87
+ Unknown paths return Foundry-style 404.
88
+
89
+ ## Compatibility matrix (MVP)
90
+
91
+ | Level | Operations |
92
+ |---|---|
93
+ | exact | list/get ontologies, object types, objects, linked objects, action types (metadata), query types |
94
+ | representative | object search (`eq`/`and`), query execute (static or callback) |
95
+ | unsupported | apply, applyBatch |
96
+
97
+ ## Security
98
+
99
+ - Do not put production secrets in fixtures or tokens used with this mock
100
+ - Tokens are compared in-process and never written to error bodies
101
+ - Query/action behavior must be allow-listed Python, never YAML eval
102
+
103
+ ## License
104
+
105
+ MIT. Palantir and Foundry are trademarks of their respective owners.
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "semblance-foundry"
7
+ version = "0.1.0"
8
+ description = "Unofficial local HTTP simulation of selected Palantir Foundry API v2 ontology operations"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{name = "Semblance Contributors"}]
13
+ keywords = ["foundry", "palantir", "mock", "simulation", "testing", "fastapi"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Testing",
22
+ ]
23
+ dependencies = [
24
+ "semblance>=0.6.1,<0.8",
25
+ "pydantic>=2.0.0",
26
+ "pyyaml>=6.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "pytest>=7.0.0",
32
+ "httpx>=0.25.0",
33
+ "ruff>=0.16.0",
34
+ "mypy>=1.0.0",
35
+ "types-PyYAML>=6.0.0",
36
+ ]
37
+ sdk = [
38
+ "foundry-platform-sdk==1.101.0",
39
+ ]
40
+
41
+ [project.scripts]
42
+ semblance-foundry = "semblance_foundry.cli:main"
43
+
44
+ [project.urls]
45
+ Documentation = "https://semblance.readthedocs.io/en/latest/guides/foundry/"
46
+ Repository = "https://github.com/eddiethedean/semblance"
47
+ Changelog = "https://github.com/eddiethedean/semblance/blob/main/packages/semblance-foundry/CHANGELOG.md"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+
52
+ [tool.setuptools.package-data]
53
+ semblance_foundry = [
54
+ "py.typed",
55
+ "compatibility.yaml",
56
+ "fixtures/defaults/*.yaml",
57
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ """
2
+ Unofficial local simulation of selected Palantir Foundry API v2 ontology operations.
3
+
4
+ Not affiliated with, endorsed by, or equivalent to Palantir Foundry.
5
+ """
6
+
7
+ from semblance_foundry.app import FoundryMock
8
+ from semblance_foundry.config import FoundryMockConfig, TokenGrant
9
+ from semblance_foundry.testing import FoundryMockContext, foundry_test_client
10
+
11
+ __all__ = [
12
+ "FoundryMock",
13
+ "FoundryMockConfig",
14
+ "FoundryMockContext",
15
+ "TokenGrant",
16
+ "foundry_test_client",
17
+ ]
@@ -0,0 +1,89 @@
1
+ """FoundryMock factory and ASGI construction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from collections.abc import Callable
7
+ from importlib.metadata import PackageNotFoundError
8
+ from importlib.metadata import version as pkg_version
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from fastapi import FastAPI
13
+
14
+ from semblance_foundry.auth import FoundryMiddleware
15
+ from semblance_foundry.compatibility import manifest_json
16
+ from semblance_foundry.config import FoundryMockConfig
17
+ from semblance_foundry.errors import register_exception_handlers
18
+ from semblance_foundry.fixtures.loaders import (
19
+ apply_fixture,
20
+ bundled_acme_path,
21
+ load_fixture_file,
22
+ )
23
+ from semblance_foundry.ids import PageTokenCodec, rid_secret
24
+ from semblance_foundry.services.ontologies import create_ontology_router
25
+ from semblance_foundry.state import FoundryState, OntologyMutations
26
+
27
+ _BUNDLED_FIXTURES = {"acme": bundled_acme_path}
28
+
29
+
30
+ def _distribution_version() -> str:
31
+ try:
32
+ return pkg_version("semblance-foundry")
33
+ except PackageNotFoundError:
34
+ return "0.1.0"
35
+
36
+
37
+ class FoundryMock:
38
+ """Fixture-backed local simulation of selected Foundry API v2 operations."""
39
+
40
+ def __init__(self, config: FoundryMockConfig | None = None) -> None:
41
+ self.config = config or FoundryMockConfig()
42
+ self.state = FoundryState(seed=self.config.seed)
43
+ self.page_token_codec = PageTokenCodec(rid_secret(self.config.seed))
44
+ self._rng = random.Random(self.config.seed)
45
+ self.ontologies = OntologyMutations(self.state)
46
+
47
+ def load_fixture(self, path: str | Path) -> FoundryMock:
48
+ doc = load_fixture_file(path)
49
+ apply_fixture(doc, self.state)
50
+ return self
51
+
52
+ def load_bundled_fixture(self, name: str = "acme") -> FoundryMock:
53
+ """Load a fixture shipped with the package (currently ``acme``)."""
54
+ loader = _BUNDLED_FIXTURES.get(name)
55
+ if loader is None:
56
+ known = ", ".join(sorted(_BUNDLED_FIXTURES))
57
+ raise ValueError(
58
+ f"unknown bundled fixture {name!r}; expected one of: {known}"
59
+ )
60
+ return self.load_fixture(loader())
61
+
62
+ def register_query(self, api_name: str, fn: Callable[..., Any]) -> None:
63
+ """Register an allow-listed Python callback for query execute."""
64
+ self.state.register_query(api_name, fn)
65
+
66
+ def as_fastapi(self) -> FastAPI:
67
+ app = FastAPI(
68
+ title="semblance-foundry",
69
+ description=(
70
+ "Unofficial local simulation of selected Palantir Foundry "
71
+ "API v2 ontology operations. Not affiliated with Palantir."
72
+ ),
73
+ version=_distribution_version(),
74
+ )
75
+ app.state.foundry_mock = self
76
+ register_exception_handlers(app, self.config.seed)
77
+ app.add_middleware(
78
+ FoundryMiddleware,
79
+ config=self.config,
80
+ rng=self._rng,
81
+ error_counter={"n": 0},
82
+ )
83
+ app.include_router(create_ontology_router())
84
+
85
+ @app.get("/.well-known/foundry-mock-compatibility.json")
86
+ def compatibility() -> dict[str, Any]:
87
+ return manifest_json()
88
+
89
+ return app
@@ -0,0 +1,167 @@
1
+ """Auth, request-id, latency, error injection, and rate-limit middleware."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import random
7
+ import time
8
+ from collections import defaultdict
9
+ from collections.abc import Callable
10
+ from threading import Lock
11
+ from typing import Any
12
+
13
+ from starlette.middleware.base import BaseHTTPMiddleware
14
+ from starlette.requests import Request
15
+ from starlette.responses import JSONResponse, Response
16
+
17
+ from semblance_foundry.config import FoundryMockConfig
18
+ from semblance_foundry.errors import foundry_error_body, next_error_instance_id
19
+
20
+ REQUEST_ID_HEADER = "X-Request-ID"
21
+
22
+
23
+ class _SlidingWindow:
24
+ def __init__(self) -> None:
25
+ self._timestamps: dict[tuple[str, str], list[float]] = defaultdict(list)
26
+ self._lock = Lock()
27
+
28
+ def allow(self, path: str, method: str, limit: float) -> bool:
29
+ if limit <= 0:
30
+ return True
31
+ now = time.monotonic()
32
+ key = (path, method)
33
+ with self._lock:
34
+ ts_list = self._timestamps[key]
35
+ ts_list[:] = [t for t in ts_list if now - t < 1.0]
36
+ if len(ts_list) >= limit:
37
+ return False
38
+ ts_list.append(now)
39
+ return True
40
+
41
+
42
+ def _bearer_token(header: str | None) -> str | None:
43
+ if header is None or header == "":
44
+ return None
45
+ parts = header.split(None, 1)
46
+ if len(parts) != 2 or parts[0].lower() != "bearer":
47
+ raise ValueError("malformed")
48
+ token = parts[1].strip()
49
+ if not token:
50
+ raise ValueError("empty")
51
+ return token
52
+
53
+
54
+ class FoundryMiddleware(BaseHTTPMiddleware):
55
+ """Auth, request-id, optional latency / error injection / rate limit."""
56
+
57
+ def __init__(
58
+ self,
59
+ app: Any,
60
+ config: FoundryMockConfig,
61
+ rng: random.Random,
62
+ error_counter: dict[str, int],
63
+ ) -> None:
64
+ super().__init__(app)
65
+ self._config = config
66
+ self._rng = rng
67
+ self._error_counter = error_counter
68
+ self._limiter = _SlidingWindow()
69
+
70
+ def _instance_id(self) -> str:
71
+ self._error_counter["n"] = self._error_counter.get("n", 0) + 1
72
+ return next_error_instance_id(
73
+ self._config.seed, self._error_counter["n"] + 10_000
74
+ )
75
+
76
+ async def dispatch(
77
+ self, request: Request, call_next: Callable[[Request], Any]
78
+ ) -> Response:
79
+ incoming = request.headers.get(REQUEST_ID_HEADER)
80
+ request_id = incoming if incoming else f"req-{self._config.seed}-{id(request)}"
81
+ request.state.request_id = request_id
82
+
83
+ if self._config.rate_limit is not None:
84
+ if not self._limiter.allow(
85
+ request.url.path, request.method, self._config.rate_limit
86
+ ):
87
+ body = foundry_error_body(
88
+ error_code="PERMISSION_DENIED",
89
+ error_name="RateLimitExceeded",
90
+ error_instance_id=self._instance_id(),
91
+ parameters={},
92
+ )
93
+ response = JSONResponse(status_code=429, content=body)
94
+ response.headers[REQUEST_ID_HEADER] = request_id
95
+ return response
96
+
97
+ auth_error = self._check_auth(request)
98
+ if auth_error is not None:
99
+ auth_error.headers[REQUEST_ID_HEADER] = request_id
100
+ return auth_error
101
+
102
+ if self._config.latency_ms or self._config.jitter_ms:
103
+ delay = self._config.latency_ms / 1000.0
104
+ if self._config.jitter_ms:
105
+ delay += self._rng.uniform(0, self._config.jitter_ms) / 1000.0
106
+ if delay > 0:
107
+ await asyncio.sleep(delay)
108
+
109
+ if self._config.error_rate > 0 and self._rng.random() < self._config.error_rate:
110
+ codes = self._config.error_codes or [500]
111
+ status = self._rng.choice(codes)
112
+ body = foundry_error_body(
113
+ error_code="INTERNAL",
114
+ error_name="InjectedError",
115
+ error_instance_id=self._instance_id(),
116
+ parameters={"statusCode": status},
117
+ )
118
+ response = JSONResponse(status_code=status, content=body)
119
+ response.headers[REQUEST_ID_HEADER] = request_id
120
+ return response
121
+
122
+ response = await call_next(request)
123
+ response.headers[REQUEST_ID_HEADER] = request_id
124
+ return response # type: ignore[no-any-return]
125
+
126
+ def _check_auth(self, request: Request) -> JSONResponse | None:
127
+ mode = self._config.auth
128
+ if mode == "disabled":
129
+ return None
130
+ header = request.headers.get("authorization")
131
+ try:
132
+ token = _bearer_token(header)
133
+ except ValueError:
134
+ return JSONResponse(
135
+ status_code=401,
136
+ content=foundry_error_body(
137
+ error_code="UNAUTHORIZED",
138
+ error_name="InvalidCredentials",
139
+ error_instance_id=self._instance_id(),
140
+ parameters={},
141
+ ),
142
+ )
143
+ if mode == "optional":
144
+ return None
145
+ # strict
146
+ if token is None:
147
+ return JSONResponse(
148
+ status_code=401,
149
+ content=foundry_error_body(
150
+ error_code="UNAUTHORIZED",
151
+ error_name="MissingCredentials",
152
+ error_instance_id=self._instance_id(),
153
+ parameters={},
154
+ ),
155
+ )
156
+ allowed = {g.token for g in self._config.tokens}
157
+ if token not in allowed:
158
+ return JSONResponse(
159
+ status_code=401,
160
+ content=foundry_error_body(
161
+ error_code="UNAUTHORIZED",
162
+ error_name="InvalidCredentials",
163
+ error_instance_id=self._instance_id(),
164
+ parameters={},
165
+ ),
166
+ )
167
+ return None