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.
- semblance_foundry-0.1.0/LICENSE.md +21 -0
- semblance_foundry-0.1.0/PKG-INFO +138 -0
- semblance_foundry-0.1.0/README.md +105 -0
- semblance_foundry-0.1.0/pyproject.toml +57 -0
- semblance_foundry-0.1.0/setup.cfg +4 -0
- semblance_foundry-0.1.0/src/semblance_foundry/__init__.py +17 -0
- semblance_foundry-0.1.0/src/semblance_foundry/app.py +89 -0
- semblance_foundry-0.1.0/src/semblance_foundry/auth.py +167 -0
- semblance_foundry-0.1.0/src/semblance_foundry/cli.py +105 -0
- semblance_foundry-0.1.0/src/semblance_foundry/compatibility.py +30 -0
- semblance_foundry-0.1.0/src/semblance_foundry/compatibility.yaml +102 -0
- semblance_foundry-0.1.0/src/semblance_foundry/config.py +43 -0
- semblance_foundry-0.1.0/src/semblance_foundry/errors.py +131 -0
- semblance_foundry-0.1.0/src/semblance_foundry/fixtures/__init__.py +15 -0
- semblance_foundry-0.1.0/src/semblance_foundry/fixtures/defaults/__init__.py +0 -0
- semblance_foundry-0.1.0/src/semblance_foundry/fixtures/defaults/acme.yaml +80 -0
- semblance_foundry-0.1.0/src/semblance_foundry/fixtures/loaders.py +239 -0
- semblance_foundry-0.1.0/src/semblance_foundry/ids.py +89 -0
- semblance_foundry-0.1.0/src/semblance_foundry/models/__init__.py +92 -0
- semblance_foundry-0.1.0/src/semblance_foundry/pagination.py +43 -0
- semblance_foundry-0.1.0/src/semblance_foundry/py.typed +0 -0
- semblance_foundry-0.1.0/src/semblance_foundry/registry.py +29 -0
- semblance_foundry-0.1.0/src/semblance_foundry/services/__init__.py +5 -0
- semblance_foundry-0.1.0/src/semblance_foundry/services/ontologies/__init__.py +5 -0
- semblance_foundry-0.1.0/src/semblance_foundry/services/ontologies/routes.py +551 -0
- semblance_foundry-0.1.0/src/semblance_foundry/state.py +281 -0
- semblance_foundry-0.1.0/src/semblance_foundry/testing.py +66 -0
- semblance_foundry-0.1.0/src/semblance_foundry.egg-info/PKG-INFO +138 -0
- semblance_foundry-0.1.0/src/semblance_foundry.egg-info/SOURCES.txt +31 -0
- semblance_foundry-0.1.0/src/semblance_foundry.egg-info/dependency_links.txt +1 -0
- semblance_foundry-0.1.0/src/semblance_foundry.egg-info/entry_points.txt +2 -0
- semblance_foundry-0.1.0/src/semblance_foundry.egg-info/requires.txt +13 -0
- 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,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
|