dirigent-testing 0.26.1__tar.gz → 0.27.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.
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/PKG-INFO +3 -3
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/pyproject.toml +3 -3
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/pyproject.toml.orig +3 -3
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/src/dirigent_testing/__init__.py +3 -1
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/src/dirigent_testing/conformance.py +15 -5
- dirigent_testing-0.27.1/src/dirigent_testing/running.py +81 -0
- dirigent_testing-0.26.1/src/dirigent_testing/running.py +0 -42
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/LICENSE +0 -0
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/README.md +0 -0
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/src/dirigent_testing/doubles.py +0 -0
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/src/dirigent_testing/environment.py +0 -0
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/src/dirigent_testing/fixtures.py +0 -0
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/src/dirigent_testing/messages.py +0 -0
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/src/dirigent_testing/py.typed +0 -0
- {dirigent_testing-0.26.1 → dirigent_testing-0.27.1}/src/dirigent_testing/wording.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirigent-testing
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.1
|
|
4
4
|
Summary: Test doubles and pytest fixtures for writing and testing dirigent blocks.
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
-
Requires-Dist: dirigent-common==0.
|
|
10
|
-
Requires-Dist: dirigent-plugin==0.
|
|
9
|
+
Requires-Dist: dirigent-common==0.27.1
|
|
10
|
+
Requires-Dist: dirigent-plugin==0.27.1
|
|
11
11
|
Requires-Dist: httpx2>=2.12.0
|
|
12
12
|
Requires-Dist: pydantic>=2.13.5
|
|
13
13
|
Requires-Dist: pytest>=9.1.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-testing"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.27.1"
|
|
4
4
|
description = "Test doubles and pytest fixtures for writing and testing dirigent blocks."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,8 +11,8 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-common==0.
|
|
15
|
-
"dirigent-plugin==0.
|
|
14
|
+
"dirigent-common==0.27.1",
|
|
15
|
+
"dirigent-plugin==0.27.1",
|
|
16
16
|
"httpx2>=2.12.0",
|
|
17
17
|
"pydantic>=2.13.5",
|
|
18
18
|
"pytest>=9.1.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-testing"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.27.1"
|
|
4
4
|
description = "Test doubles and pytest fixtures for writing and testing dirigent blocks."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,8 +11,8 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-common==0.
|
|
15
|
-
"dirigent-plugin==0.
|
|
14
|
+
"dirigent-common==0.27.1",
|
|
15
|
+
"dirigent-plugin==0.27.1",
|
|
16
16
|
"httpx2>=2.12.0",
|
|
17
17
|
"pydantic>=2.13.5",
|
|
18
18
|
"pytest>=9.1.1",
|
|
@@ -4,7 +4,7 @@ from dirigent_testing.conformance import assert_contribution_conforms, check_pac
|
|
|
4
4
|
from dirigent_testing.doubles import FakeCapture, FakeContext, FakeRuns, FakeSink, FakeStorage, RecordingLogger
|
|
5
5
|
from dirigent_testing.environment import CONFIGURING_PREFIXES, TEST_WIDTH, pin_terminal, scrub_configuration
|
|
6
6
|
from dirigent_testing.fixtures import no_connection_outlives_its_loop
|
|
7
|
-
from dirigent_testing.running import call_block, carry_cursor
|
|
7
|
+
from dirigent_testing.running import FakeReceiveContext, call_block, carry_cursor, receive_request
|
|
8
8
|
from dirigent_testing.wording import check_pack_messages
|
|
9
9
|
|
|
10
10
|
__all__ = [
|
|
@@ -12,6 +12,7 @@ __all__ = [
|
|
|
12
12
|
"TEST_WIDTH",
|
|
13
13
|
"FakeCapture",
|
|
14
14
|
"FakeContext",
|
|
15
|
+
"FakeReceiveContext",
|
|
15
16
|
"FakeRuns",
|
|
16
17
|
"FakeSink",
|
|
17
18
|
"FakeStorage",
|
|
@@ -23,5 +24,6 @@ __all__ = [
|
|
|
23
24
|
"check_pack_messages",
|
|
24
25
|
"no_connection_outlives_its_loop",
|
|
25
26
|
"pin_terminal",
|
|
27
|
+
"receive_request",
|
|
26
28
|
"scrub_configuration",
|
|
27
29
|
]
|
|
@@ -19,7 +19,7 @@ import yaml
|
|
|
19
19
|
from jsonschema import Draft202012Validator
|
|
20
20
|
from jsonschema import ValidationError as SchemaValidationError
|
|
21
21
|
from jsonschema.validators import extend as extend_validator # pyright: ignore[reportUnknownVariableType]
|
|
22
|
-
from pydantic import JsonValue
|
|
22
|
+
from pydantic import BaseModel, JsonValue
|
|
23
23
|
|
|
24
24
|
from dirigent_common import BlockModel, HumaneJsonSchema, JsonMap
|
|
25
25
|
from dirigent_plugin import AnyOperator, AnySensor, Contribution, mark_refusal
|
|
@@ -191,10 +191,11 @@ def assert_contribution_conforms(contribution: Contribution) -> list[str]:
|
|
|
191
191
|
A ``Contribution`` already rejects colliding ids when it is built, and a block id is a
|
|
192
192
|
pattern that forbids an empty one, so a valid contribution passes those. This adds the
|
|
193
193
|
check the contract cannot make on its own: that every operator's and sensor's
|
|
194
|
-
``config_model`` and ``output_model`` is a :class:`BlockModel
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
``config_model`` and ``output_model`` is a :class:`BlockModel`, and every source kind's is a
|
|
195
|
+
pydantic model. A connection or source kind's ``mark`` is checked through
|
|
196
|
+
:func:`dirigent_plugin.mark_refusal`, which is what registration holds it to, so a pack
|
|
197
|
+
reads the same refusal here as it would at startup; a kind that declares none passes. An
|
|
198
|
+
empty list means the contribution conforms.
|
|
198
199
|
"""
|
|
199
200
|
issues: list[str] = []
|
|
200
201
|
seen: set[str] = set()
|
|
@@ -214,4 +215,13 @@ def assert_contribution_conforms(contribution: Contribution) -> list[str]:
|
|
|
214
215
|
refusal = mark_refusal(connection.id, connection.mark)
|
|
215
216
|
if refusal is not None:
|
|
216
217
|
issues.append(refusal)
|
|
218
|
+
for source in contribution.sources:
|
|
219
|
+
kind = source.spec.kind
|
|
220
|
+
for role in ("config_model", "output_model"):
|
|
221
|
+
model = getattr(source, role, None)
|
|
222
|
+
if not (isinstance(model, type) and issubclass(model, BaseModel)):
|
|
223
|
+
issues.append(f"source kind {kind} {role} is not a pydantic model")
|
|
224
|
+
refusal = mark_refusal(kind, source.mark)
|
|
225
|
+
if refusal is not None:
|
|
226
|
+
issues.append(refusal)
|
|
217
227
|
return issues
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Making one block call from a test, through the validation the engine puts in front of it."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from jsonschema import FormatChecker
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from dirigent_common.formats import base_format_checker
|
|
10
|
+
from dirigent_plugin import AnyOperator, AnySensor, Logger, NotYet, Operator, StepContext
|
|
11
|
+
from dirigent_plugin.sources import Ignore, InboundRequest, ReceivedSource, Refusal
|
|
12
|
+
from dirigent_testing.doubles import FakeContext, RecordingLogger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def call_block(
|
|
16
|
+
block: AnyOperator | AnySensor,
|
|
17
|
+
config: Mapping[str, Any],
|
|
18
|
+
ctx: StepContext | FakeContext,
|
|
19
|
+
) -> BaseModel:
|
|
20
|
+
"""Validate the config the way the engine would, then make the block's first call.
|
|
21
|
+
|
|
22
|
+
An operator's execute or a sensor's poke, with the validated config -- so a test
|
|
23
|
+
exercises the same validation path a real run does, and a config the schema refuses
|
|
24
|
+
fails here rather than silently succeeding with defaults.
|
|
25
|
+
"""
|
|
26
|
+
context = ctx.as_context() if isinstance(ctx, FakeContext) else ctx
|
|
27
|
+
validated = block.config_model.model_validate(config)
|
|
28
|
+
if isinstance(block, Operator):
|
|
29
|
+
return await block.execute(validated, context)
|
|
30
|
+
return await block.poke(validated, context)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def carry_cursor(sensor: AnySensor, answer: BaseModel, ctx: FakeContext) -> None:
|
|
34
|
+
"""Hand the next poke the cursor the engine would, after one poke answered this.
|
|
35
|
+
|
|
36
|
+
A park that returned a cursor replaces the one the context held, and one that returned
|
|
37
|
+
none leaves it. A success hands on what :meth:`Sensor.resume_cursor` says, as a
|
|
38
|
+
``sensor`` source hands it to the poke that starts its next wait.
|
|
39
|
+
"""
|
|
40
|
+
if isinstance(answer, NotYet):
|
|
41
|
+
if answer.cursor is not None:
|
|
42
|
+
ctx.cursor = dict(answer.cursor)
|
|
43
|
+
return
|
|
44
|
+
resumed = sensor.resume_cursor(answer)
|
|
45
|
+
ctx.cursor = dict(resumed) if resumed is not None else None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FakeReceiveContext:
|
|
49
|
+
"""What a received kind may reach while it turns a request into an input, with its log kept."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, format_checker: FormatChecker | None = None) -> None:
|
|
52
|
+
"""Start with an empty log and the base format checker unless one is given."""
|
|
53
|
+
self.logger = RecordingLogger()
|
|
54
|
+
self._format_checker = format_checker or base_format_checker()
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def log(self) -> Logger:
|
|
58
|
+
"""Where the kind says what it did with the request."""
|
|
59
|
+
return self.logger
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def format_checker(self) -> FormatChecker:
|
|
63
|
+
"""The checker the kind's input is held to."""
|
|
64
|
+
return self._format_checker
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def receive_request(
|
|
68
|
+
source: ReceivedSource[Any, Any],
|
|
69
|
+
config: Mapping[str, Any],
|
|
70
|
+
request: InboundRequest,
|
|
71
|
+
ctx: FakeReceiveContext | None = None,
|
|
72
|
+
) -> BaseModel | Ignore | Refusal:
|
|
73
|
+
"""Validate the config, then take a request the way the intake does: its head, then its body.
|
|
74
|
+
|
|
75
|
+
A refusal on the head alone is answered before the body is read, as the server answers it.
|
|
76
|
+
"""
|
|
77
|
+
validated = source.config_model.model_validate(config)
|
|
78
|
+
refused = source.admit(validated, request)
|
|
79
|
+
if refused is not None:
|
|
80
|
+
return refused
|
|
81
|
+
return await source.receive(validated, request, ctx or FakeReceiveContext())
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"""Making one block call from a test, through the validation the engine puts in front of it."""
|
|
2
|
-
|
|
3
|
-
from collections.abc import Mapping
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from pydantic import BaseModel
|
|
7
|
-
|
|
8
|
-
from dirigent_plugin import AnyOperator, AnySensor, NotYet, Operator, StepContext
|
|
9
|
-
from dirigent_testing.doubles import FakeContext
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
async def call_block(
|
|
13
|
-
block: AnyOperator | AnySensor,
|
|
14
|
-
config: Mapping[str, Any],
|
|
15
|
-
ctx: StepContext | FakeContext,
|
|
16
|
-
) -> BaseModel:
|
|
17
|
-
"""Validate the config the way the engine would, then make the block's first call.
|
|
18
|
-
|
|
19
|
-
An operator's execute or a sensor's poke, with the validated config -- so a test
|
|
20
|
-
exercises the same validation path a real run does, and a config the schema refuses
|
|
21
|
-
fails here rather than silently succeeding with defaults.
|
|
22
|
-
"""
|
|
23
|
-
context = ctx.as_context() if isinstance(ctx, FakeContext) else ctx
|
|
24
|
-
validated = block.config_model.model_validate(config)
|
|
25
|
-
if isinstance(block, Operator):
|
|
26
|
-
return await block.execute(validated, context)
|
|
27
|
-
return await block.poke(validated, context)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def carry_cursor(sensor: AnySensor, answer: BaseModel, ctx: FakeContext) -> None:
|
|
31
|
-
"""Hand the next poke the cursor the engine would, after one poke answered this.
|
|
32
|
-
|
|
33
|
-
A park that returned a cursor replaces the one the context held, and one that returned
|
|
34
|
-
none leaves it. A success hands on what :meth:`Sensor.resume_cursor` says, as a watch
|
|
35
|
-
hands it to the first poke of its next run.
|
|
36
|
-
"""
|
|
37
|
-
if isinstance(answer, NotYet):
|
|
38
|
-
if answer.cursor is not None:
|
|
39
|
-
ctx.cursor = dict(answer.cursor)
|
|
40
|
-
return
|
|
41
|
-
resumed = sensor.resume_cursor(answer)
|
|
42
|
-
ctx.cursor = dict(resumed) if resumed is not None else None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|