specrig 0.3.0__tar.gz → 0.3.1__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.
- {specrig-0.3.0 → specrig-0.3.1}/PKG-INFO +4 -2
- {specrig-0.3.0 → specrig-0.3.1}/README.md +3 -1
- {specrig-0.3.0 → specrig-0.3.1}/docs/design.md +2 -2
- {specrig-0.3.0 → specrig-0.3.1}/pyproject.toml +1 -1
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/specrig.md +5 -2
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/specrig.md +5 -2
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/backend.py +25 -8
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/tool/templates.py +4 -1
- {specrig-0.3.0 → specrig-0.3.1}/tests/test_backend.py +69 -0
- {specrig-0.3.0 → specrig-0.3.1}/.github/workflows/release.yml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/.gitignore +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/LICENSE +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/Makefile +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/docs/billing.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/AGENTS.md +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/CLAUDE.md +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/config/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/config/settings.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/config/urls.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/config/wsgi.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/manage.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/pyproject.toml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/reminders/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/reminders/billing.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/reminders/jobs.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/reminders/management/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/reminders/management/commands/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/reminders/management/commands/remind.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/reminders/migrations/0001_initial.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/reminders/migrations/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/reminders/models.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/reminders/views.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/specrig.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/tests/checks.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/tests/features/history.feature +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/tests/features/reminders.feature +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/tests/flows.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/tests/internal/boot.feature +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/tests/internal/harness.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/tests/internal/test_cases.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/tests/steps/history.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/tests/steps/reminders.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/reminders/tests/surface.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/AGENTS.md +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/CLAUDE.md +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/alembic.ini +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/migrations/env.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/migrations/versions/0001_orders.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/pyproject.toml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/shop/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/shop/billing.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/shop/cli.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/shop/db.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/shop/jobs.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/shop/main.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/shop/settings.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/specrig.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/tests/checks.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/tests/features/orders.feature +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/tests/features/placing.feature +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/tests/flows.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/tests/internal/boot.feature +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/tests/internal/harness.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/tests/internal/test_cases.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/tests/steps/orders.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/tests/steps/placing.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/samples/shop/tests/surface.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/__main__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/_yaml.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/asgi.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/clock.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/code/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/code/calls.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/code/django.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/code/fastapi.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/code/index.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/code/settings.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/contract.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/django/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/docker.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/fastapi/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/flows.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/gate.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/http.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/postgres.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/protocol/agents/contributing.md +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/protocol/agents/header.md +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/protocol/agents/reference.md +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/protocol/agents/rule11.md +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/protocol/agents/short.md +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/protocol/modes.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/proxy.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/py.typed +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/records.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/redis.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/resource.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/rig.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/run.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/standins.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/suite.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/surface.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/tool/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/tool/agents.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/tool/cli.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/tool/init.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/tool/modes.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/src/specrig/tool/service.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/django/manage.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/django/pyproject.toml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/django/stock/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/django/stock/api.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/django/stock/settings.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/django/stock/urls.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/django/stock/views.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/django/stock/warehouse.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/fastapi/.env.example +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/fastapi/gateway/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/fastapi/gateway/config.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/fastapi/gateway/crm.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/fastapi/gateway/main.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/fastapi/gateway/routes/__init__.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/fastapi/gateway/routes/orders.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/fastapi/gateway/sessions.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/fastapi/pyproject.toml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/protocol/agents/verdicts.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/protocol/markers/areas.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/protocol/records/reminders.flows.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/protocol/records/reminders.trace.json +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/protocol/records/shop.flows.yaml +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/fixtures/protocol/records/shop.trace.json +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/test_agents.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/test_flows.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/test_format.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/test_gate.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/test_harness.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/test_init.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/test_postgres.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/test_rules.py +0 -0
- {specrig-0.3.0 → specrig-0.3.1}/tests/test_surface.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: specrig
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A harness for a FastAPI or Django service developed specification first
|
|
5
5
|
Project-URL: Homepage, https://github.com/specrig/specrig-python
|
|
6
6
|
Project-URL: Protocol, https://github.com/specrig/protocol
|
|
@@ -243,7 +243,9 @@ fixtures, autouse ones included, would change the service the cases see.
|
|
|
243
243
|
|---|---|
|
|
244
244
|
| `harness.<backend>.reply(method, path, status, body, headers)` | what the neighbour answers; `headers`, strings to strings, are added to the response the client sees. A segment in braces, `/orders/{id}`, takes any one segment, the exact path wins over a template and the first declared of two templates over the second — in every call here that names a path |
|
|
245
245
|
| `harness.<backend>.reply_to(method, path, query, status, body, headers)` | the same, for a request whose query has these values; the reply naming the most of them wins |
|
|
246
|
-
| `harness.<backend>.
|
|
246
|
+
| `harness.<backend>.reply_with(method, path, answer, headers)` | the answer worked out from each request: `answer` takes what `requests` returns one of and gives `(status, body)`; a reply naming no query, checked as the others |
|
|
247
|
+
| `harness.<backend>.time_out(method, path, query=None)` | the neighbour does not answer in time: the client meets the timeout it would meet, at once; no reply lifts it |
|
|
248
|
+
| `harness.<backend>.heal()` | the neighbour answers again: every `time_out` is lifted, the replies stay |
|
|
247
249
|
| `harness.<backend>.requests(method, path)` | what reached the neighbour: `method`, `path`, `query` and `json`, its decoded body; a template returns the requests it takes |
|
|
248
250
|
| `harness.http()` | a client of the service's own stack: `get`, `post`, `put`, `patch`, `delete`, `send`; a response with `status`, `headers`, `text`, `json()` |
|
|
249
251
|
| `harness.run(job, *args, **kwargs)` | a job — see below |
|
|
@@ -215,7 +215,9 @@ fixtures, autouse ones included, would change the service the cases see.
|
|
|
215
215
|
|---|---|
|
|
216
216
|
| `harness.<backend>.reply(method, path, status, body, headers)` | what the neighbour answers; `headers`, strings to strings, are added to the response the client sees. A segment in braces, `/orders/{id}`, takes any one segment, the exact path wins over a template and the first declared of two templates over the second — in every call here that names a path |
|
|
217
217
|
| `harness.<backend>.reply_to(method, path, query, status, body, headers)` | the same, for a request whose query has these values; the reply naming the most of them wins |
|
|
218
|
-
| `harness.<backend>.
|
|
218
|
+
| `harness.<backend>.reply_with(method, path, answer, headers)` | the answer worked out from each request: `answer` takes what `requests` returns one of and gives `(status, body)`; a reply naming no query, checked as the others |
|
|
219
|
+
| `harness.<backend>.time_out(method, path, query=None)` | the neighbour does not answer in time: the client meets the timeout it would meet, at once; no reply lifts it |
|
|
220
|
+
| `harness.<backend>.heal()` | the neighbour answers again: every `time_out` is lifted, the replies stay |
|
|
219
221
|
| `harness.<backend>.requests(method, path)` | what reached the neighbour: `method`, `path`, `query` and `json`, its decoded body; a template returns the requests it takes |
|
|
220
222
|
| `harness.http()` | a client of the service's own stack: `get`, `post`, `put`, `patch`, `delete`, `send`; a response with `status`, `headers`, `text`, `json()` |
|
|
221
223
|
| `harness.run(job, *args, **kwargs)` | a job — see below |
|
|
@@ -126,8 +126,8 @@ and `requests`' `HTTPAdapter`. Every other client meets the proxy.
|
|
|
126
126
|
|
|
127
127
|
**A template names what a step answers, not what the service sent.** As in
|
|
128
128
|
specrig-js, a path segment in braces takes any one segment, the exact path wins,
|
|
129
|
-
and of two templates the first declared; `reply`, `reply_to`, `
|
|
130
|
-
`requests` all take one. The contract check and the trace go on naming a request
|
|
129
|
+
and of two templates the first declared; `reply`, `reply_to`, `reply_with`,
|
|
130
|
+
`time_out` and `requests` all take one. The contract check and the trace go on naming a request
|
|
131
131
|
by the path it came with, so a template that matches too much is not hidden by
|
|
132
132
|
the contract it is checked against. A reply's headers are added to the response
|
|
133
133
|
as the transports hand it to the client, so `httpx`, `httpx2` and `requests` see
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# specrig.md
|
|
2
2
|
|
|
3
3
|
How an agent works in this service under specrig: Rule #11, and how the suite runs.
|
|
4
|
-
`../../.venv/bin/specrig agents` writes this file with `specrig` 0.3.
|
|
4
|
+
`../../.venv/bin/specrig agents` writes this file with `specrig` 0.3.1, and an edit here is lost on the next
|
|
5
5
|
run; `AGENTS.md` beside it is the service's own.
|
|
6
6
|
|
|
7
7
|
**Read `CONTRIBUTING.md` first, if it exists.** It holds service-specific
|
|
@@ -400,7 +400,10 @@ neighbour `harness.py` names:
|
|
|
400
400
|
every call below, and the exact path wins;
|
|
401
401
|
- `harness.<neighbour>.reply_to(method, path, query, status, body)` — the same, for a
|
|
402
402
|
request whose query has these values;
|
|
403
|
-
- `harness.<neighbour>.
|
|
403
|
+
- `harness.<neighbour>.reply_with(method, path, answer)` — the answer worked out from
|
|
404
|
+
each request, `(status, body)`;
|
|
405
|
+
- `harness.<neighbour>.time_out(method, path)` — the neighbour not answering in time,
|
|
406
|
+
until `harness.<neighbour>.heal()`;
|
|
404
407
|
- `harness.<neighbour>.requests(method, path)` — what reached the neighbour, for a
|
|
405
408
|
`Then`.
|
|
406
409
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# specrig.md
|
|
2
2
|
|
|
3
3
|
How an agent works in this service under specrig: Rule #11, and how the suite runs.
|
|
4
|
-
`../../.venv/bin/specrig agents` writes this file with `specrig` 0.3.
|
|
4
|
+
`../../.venv/bin/specrig agents` writes this file with `specrig` 0.3.1, and an edit here is lost on the next
|
|
5
5
|
run; `AGENTS.md` beside it is the service's own.
|
|
6
6
|
|
|
7
7
|
**Read `CONTRIBUTING.md` first, if it exists.** It holds service-specific
|
|
@@ -400,7 +400,10 @@ neighbour `harness.py` names:
|
|
|
400
400
|
every call below, and the exact path wins;
|
|
401
401
|
- `harness.<neighbour>.reply_to(method, path, query, status, body)` — the same, for a
|
|
402
402
|
request whose query has these values;
|
|
403
|
-
- `harness.<neighbour>.
|
|
403
|
+
- `harness.<neighbour>.reply_with(method, path, answer)` — the answer worked out from
|
|
404
|
+
each request, `(status, body)`;
|
|
405
|
+
- `harness.<neighbour>.time_out(method, path)` — the neighbour not answering in time,
|
|
406
|
+
until `harness.<neighbour>.heal()`;
|
|
404
407
|
- `harness.<neighbour>.requests(method, path)` — what reached the neighbour, for a
|
|
405
408
|
`Then`.
|
|
406
409
|
|
|
@@ -27,6 +27,9 @@ class Received:
|
|
|
27
27
|
json: Any
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
Answer = Callable[[Received], tuple[int, Any]]
|
|
31
|
+
|
|
32
|
+
|
|
30
33
|
class StandIn(ABC):
|
|
31
34
|
origin: str
|
|
32
35
|
setting: str | None
|
|
@@ -55,7 +58,7 @@ class Backend(StandIn):
|
|
|
55
58
|
self.name = ""
|
|
56
59
|
self._trace = Trace()
|
|
57
60
|
self._checked: Contract | None = None
|
|
58
|
-
self._replies: dict[str, list[tuple[dict[str, list[str]],
|
|
61
|
+
self._replies: dict[str, list[tuple[dict[str, list[str]], Answer, dict[str, str]]]] = {}
|
|
59
62
|
self._timeouts: dict[str, list[dict[str, list[str]]]] = {}
|
|
60
63
|
self._received: list[Received] = []
|
|
61
64
|
self._unanswered: list[str] = []
|
|
@@ -85,17 +88,28 @@ class Backend(StandIn):
|
|
|
85
88
|
|
|
86
89
|
def reply_to(
|
|
87
90
|
self, method: str, path: str, query: Query, status: int, body: Any = None, headers: Headers | None = None
|
|
91
|
+
) -> None:
|
|
92
|
+
declared = copy.deepcopy(body)
|
|
93
|
+
self._declare(method, path, _listed(query), lambda _: (status, declared), headers)
|
|
94
|
+
|
|
95
|
+
def reply_with(self, method: str, path: str, answer: Answer, headers: Headers | None = None) -> None:
|
|
96
|
+
self._declare(method, path, {}, answer, headers)
|
|
97
|
+
|
|
98
|
+
def _declare(
|
|
99
|
+
self, method: str, path: str, asked: dict[str, list[str]], answer: Answer, headers: Headers | None
|
|
88
100
|
) -> None:
|
|
89
101
|
key = f"{method.upper()} {path}"
|
|
90
|
-
asked = _listed(query)
|
|
91
102
|
kept = [r for r in self._replies.get(key, []) if r[0] != asked]
|
|
92
|
-
self._replies[key] = [*kept, (asked,
|
|
103
|
+
self._replies[key] = [*kept, (asked, answer, dict(headers or {}))]
|
|
93
104
|
|
|
94
105
|
def time_out(self, method: str, path: str, query: Query | None = None) -> None:
|
|
95
106
|
key = f"{method.upper()} {path}"
|
|
96
107
|
asked = _listed(query or {})
|
|
97
108
|
self._timeouts[key] = [*(q for q in self._timeouts.get(key, []) if q != asked), asked]
|
|
98
109
|
|
|
110
|
+
def heal(self) -> None:
|
|
111
|
+
self._timeouts = {}
|
|
112
|
+
|
|
99
113
|
def requests(self, method: str, path: str) -> list[Received]:
|
|
100
114
|
return [r for r in self._received if r.method == method.upper() and _takes(path, r.path)]
|
|
101
115
|
|
|
@@ -107,22 +121,25 @@ class Backend(StandIn):
|
|
|
107
121
|
body = http.decoded(request.body)
|
|
108
122
|
call = self._trace.begin()
|
|
109
123
|
try:
|
|
110
|
-
|
|
124
|
+
received = Received(method, path, asked, body)
|
|
125
|
+
self._received.append(received)
|
|
111
126
|
if self._checked is not None:
|
|
112
127
|
self._violations.extend(self._checked.request(method, path, body))
|
|
113
128
|
if _pick(_declared(self._timeouts, method, path), asked, lambda q: q) is not None:
|
|
114
129
|
raise Unreachable(f"{self.name} did not answer {key} in time", timed_out=True)
|
|
115
|
-
return self._fulfil(
|
|
130
|
+
return self._fulfil(received, key + request.search())
|
|
116
131
|
finally:
|
|
117
132
|
name = self._checked.name(method, path) if self._checked is not None else key
|
|
118
133
|
self._trace.end(call, "out", "http", name, body)
|
|
119
134
|
|
|
120
|
-
def _fulfil(self,
|
|
121
|
-
|
|
135
|
+
def _fulfil(self, received: Received, asked: str) -> Response:
|
|
136
|
+
method, path = received.method, received.path
|
|
137
|
+
reply = _pick(_declared(self._replies, method, path), received.query, lambda r: r[0])
|
|
122
138
|
if reply is None:
|
|
123
139
|
self._unanswered.append(asked)
|
|
124
140
|
return _respond(501, {"code": "unanswered", "message": f"{method} {path}"})
|
|
125
|
-
_,
|
|
141
|
+
_, answer, headers = reply
|
|
142
|
+
status, body = answer(received)
|
|
126
143
|
if self._checked is not None:
|
|
127
144
|
self._violations.extend(self._checked.response(method, path, status, {} if body is None else body))
|
|
128
145
|
return _respond(status, None if status == 204 else ({} if body is None else body), headers)
|
|
@@ -166,7 +166,10 @@ neighbour `harness.py` names:
|
|
|
166
166
|
every call below, and the exact path wins;
|
|
167
167
|
- `harness.<neighbour>.reply_to(method, path, query, status, body)` — the same, for a
|
|
168
168
|
request whose query has these values;
|
|
169
|
-
- `harness.<neighbour>.
|
|
169
|
+
- `harness.<neighbour>.reply_with(method, path, answer)` — the answer worked out from
|
|
170
|
+
each request, `(status, body)`;
|
|
171
|
+
- `harness.<neighbour>.time_out(method, path)` — the neighbour not answering in time,
|
|
172
|
+
until `harness.<neighbour>.heal()`;
|
|
170
173
|
- `harness.<neighbour>.requests(method, path)` — what reached the neighbour, for a
|
|
171
174
|
`Then`.
|
|
172
175
|
|
|
@@ -178,6 +178,75 @@ def test_a_neighbour_that_does_not_answer_in_time_is_unreachable_by_template_too
|
|
|
178
178
|
assert get(backend, "/payments") == 200
|
|
179
179
|
|
|
180
180
|
|
|
181
|
+
def test_heal_lifts_every_time_out_and_keeps_the_replies() -> None:
|
|
182
|
+
backend, _ = billing()
|
|
183
|
+
backend.reply("GET", "/invoices", 200, [])
|
|
184
|
+
backend.time_out("GET", "/invoices/7")
|
|
185
|
+
backend.time_out("GET", "/invoices", {"status": "paid"})
|
|
186
|
+
backend.reply("GET", "/invoices/7", 200, INVOICE)
|
|
187
|
+
|
|
188
|
+
with pytest.raises(Unreachable):
|
|
189
|
+
get(backend, "/invoices/7")
|
|
190
|
+
backend.heal()
|
|
191
|
+
|
|
192
|
+
assert get(backend, "/invoices/7") == 200
|
|
193
|
+
assert get(backend, "/invoices?status=paid") == 200
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_an_answer_is_worked_out_from_each_request() -> None:
|
|
197
|
+
backend, _ = billing()
|
|
198
|
+
backend.reply_with(
|
|
199
|
+
"GET",
|
|
200
|
+
"/invoices/{order}",
|
|
201
|
+
lambda r: (200, {**INVOICE, "order": int(r.path.rsplit("/", 1)[1])}),
|
|
202
|
+
{"X-Kind": "own"},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
for order in (7, 8):
|
|
206
|
+
answered = backend.answer(Request("GET", f"https://billing.shop.test/invoices/{order}"))
|
|
207
|
+
assert (answered.status, answered.json()["order"], answered.headers["X-Kind"]) == (200, order, "own")
|
|
208
|
+
assert backend.failures() == []
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_an_answer_may_change_with_what_reached_the_neighbour() -> None:
|
|
212
|
+
backend, _ = billing()
|
|
213
|
+
backend.reply_with(
|
|
214
|
+
"GET",
|
|
215
|
+
"/invoices/7",
|
|
216
|
+
lambda r: (
|
|
217
|
+
(503, {"code": "busy", "message": "later"})
|
|
218
|
+
if len(backend.requests("GET", "/invoices/7")) == 1
|
|
219
|
+
else (200, INVOICE)
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
assert [get(backend, "/invoices/7"), get(backend, "/invoices/7")] == [503, 200]
|
|
224
|
+
assert backend.failures() == []
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_an_answer_worked_out_is_a_declaration_naming_no_query() -> None:
|
|
228
|
+
backend, _ = billing()
|
|
229
|
+
backend.reply_with("GET", "/invoices", lambda r: (200, [INVOICE]))
|
|
230
|
+
backend.reply_to("GET", "/invoices", {"status": "paid"}, 200, [])
|
|
231
|
+
|
|
232
|
+
def listed(path: str) -> list[object]:
|
|
233
|
+
return backend.answer(Request("GET", f"https://billing.shop.test{path}")).json()
|
|
234
|
+
|
|
235
|
+
assert (listed("/invoices?status=unpaid"), listed("/invoices?status=paid")) == ([INVOICE], [])
|
|
236
|
+
backend.reply("GET", "/invoices", 200, [])
|
|
237
|
+
assert listed("/invoices") == []
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_the_contract_checks_an_answer_worked_out() -> None:
|
|
241
|
+
backend, _ = billing()
|
|
242
|
+
backend.reply_with("GET", "/invoices/{order}", lambda r: (200, {"order": 7}))
|
|
243
|
+
|
|
244
|
+
get(backend, "/invoices/7")
|
|
245
|
+
|
|
246
|
+
[failure] = backend.failures()
|
|
247
|
+
assert "GET /invoices/7 answered 200: $ 'amount' is a required property" in failure
|
|
248
|
+
|
|
249
|
+
|
|
181
250
|
def test_the_contract_check_and_the_trace_name_a_request_by_its_real_path() -> None:
|
|
182
251
|
backend, trace = billing()
|
|
183
252
|
backend.reply("GET", "/invoices/{order}", 200, {"order": 7})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|