solverforge 0.6.8__tar.gz → 0.6.10__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.
- {solverforge-0.6.8 → solverforge-0.6.10}/Cargo.lock +1 -1
- {solverforge-0.6.8 → solverforge-0.6.10}/Cargo.toml +1 -1
- {solverforge-0.6.8 → solverforge-0.6.10}/PKG-INFO +23 -10
- {solverforge-0.6.8 → solverforge-0.6.10}/README.md +22 -9
- {solverforge-0.6.8 → solverforge-0.6.10}/pyproject.toml +5 -1
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/__init__.py +3 -1
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/_native.pyi +2 -6
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/config.py +1 -3
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/constraints.py +8 -25
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/decorators.py +15 -4
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/errors.py +0 -1
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/events.py +0 -1
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/fields.py +20 -25
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/groups.py +4 -13
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/model.py +6 -9
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/solver.py +1 -3
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/typing.py +0 -1
- {solverforge-0.6.8 → solverforge-0.6.10}/src/schema/build.rs +18 -1
- {solverforge-0.6.8 → solverforge-0.6.10}/src/schema/parse.rs +20 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/schema/types.rs +4 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/state/entity_table.rs +5 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/state/marshal.rs +38 -1
- {solverforge-0.6.8 → solverforge-0.6.10}/src/state/solution.rs +1 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/LICENSE +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/console.py +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/joiner.py +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/manager.py +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/py.typed +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/score.py +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/python/solverforge/ui.py +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/rust-toolchain.toml +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/bindings.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/callbacks/call.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/callbacks/mod.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/callbacks/registry.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/callbacks/threading.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/config.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/constraints/compiled.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/constraints/evaluate.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/constraints/incremental.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/constraints/list_precedence.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/constraints/matches.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/constraints/mod.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/constraints/state.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/constraints/stream_plan.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/descriptor/extractor.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/descriptor/mod.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/descriptor/variable.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/error.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/intern.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/lib.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/manager/candidate_trace.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/manager/events.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/manager/jobs.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/manager/mod.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/manager/provenance.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/proxy/entity.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/proxy/list.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/proxy/mod.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/proxy/solution.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/runtime/candidate_metric.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/runtime/dynamic_assignment_group.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/runtime/dynamic_provider.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/runtime/list_slots.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/runtime/mod.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/runtime/scalar_slots.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/schema/compiled.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/schema/mod.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/schema/python.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/schema/runtime_plan.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/schema/validate.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/score.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/solver/api.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/solver/console.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/solver/mod.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/solver/progress.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/solver/result.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/solver/solvable.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/state/callback_view.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/state/clone.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/state/mod.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/state/variables.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/ui.rs +0 -0
- {solverforge-0.6.8 → solverforge-0.6.10}/src/value.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solverforge
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.10
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -51,12 +51,12 @@ requirements, and other APIs that are not part of this package.
|
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
53
53
|
|
|
54
|
-
The package version prepared by this checkout is `solverforge` `0.6.
|
|
54
|
+
The package version prepared by this checkout is `solverforge` `0.6.10`, targeting
|
|
55
55
|
the published and registry-locked SolverForge Rust `0.19.7` dependency base.
|
|
56
56
|
Install the matching package after its Python release with:
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
|
-
python3.14 -m pip install "solverforge==0.6.
|
|
59
|
+
python3.14 -m pip install "solverforge==0.6.10"
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
The installable wheel contains the core `solverforge` package, native extension,
|
|
@@ -76,12 +76,12 @@ make deliveries-run PORT=7861
|
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
The same source-checkout examples can be run against the matching published
|
|
79
|
-
package once `0.6.
|
|
79
|
+
package once `0.6.10` is released, without `PYTHONPATH` or an editable checkout:
|
|
80
80
|
|
|
81
81
|
```sh
|
|
82
82
|
python3.14 -m venv .venv-examples
|
|
83
83
|
. .venv-examples/bin/activate
|
|
84
|
-
python -m pip install "solverforge[examples]==0.6.
|
|
84
|
+
python -m pip install "solverforge[examples]==0.6.10"
|
|
85
85
|
python examples/nqueens.py
|
|
86
86
|
python -m examples.solverforge_hospital
|
|
87
87
|
python -m examples.solverforge_deliveries
|
|
@@ -104,16 +104,19 @@ from solverforge import (
|
|
|
104
104
|
Solver,
|
|
105
105
|
constraint_provider,
|
|
106
106
|
planning_entity,
|
|
107
|
+
planning_pin,
|
|
107
108
|
planning_solution,
|
|
108
109
|
planning_variable,
|
|
109
110
|
)
|
|
110
111
|
|
|
111
112
|
@planning_entity
|
|
112
113
|
class Shift:
|
|
114
|
+
pinned = planning_pin()
|
|
113
115
|
nurse = planning_variable(value_range_provider="nurses", allows_unassigned=True)
|
|
114
116
|
|
|
115
117
|
def __init__(self, required: bool = True, nurse: int | None = None) -> None:
|
|
116
118
|
self.required = required
|
|
119
|
+
self.pinned = False
|
|
117
120
|
self.nurse = nurse
|
|
118
121
|
|
|
119
122
|
@constraint_provider
|
|
@@ -243,11 +246,21 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
243
246
|
per-query Python callbacks. Provider-backed value ranges are imported once per
|
|
244
247
|
variable and shared across rows in Rust-owned state. Row candidate callbacks
|
|
245
248
|
remain row-specific and define move legality throughout construction and local
|
|
246
|
-
search, including assignment-group swaps and rematches.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
input
|
|
249
|
+
search, including assignment-group swaps and rematches.
|
|
250
|
+
- `planning_pin()` declares the entity attribute that holds an entity's pinned
|
|
251
|
+
flag. The declared attribute must hold a `bool` on every instance; an unset or
|
|
252
|
+
non-`bool` value is rejected at import rather than read as "not pinned". A
|
|
253
|
+
pinned entity keeps its input planning state: its genuine scalar value and the
|
|
254
|
+
elements its list variable owns survive construction, local search,
|
|
255
|
+
ruin/recreate, and exhaustive search, while every unpinned entity in the same
|
|
256
|
+
solve is still constructed and searched. Pinning preserves input state, it does
|
|
257
|
+
not exempt a row from the mandatory-completion gate: a pinned required scalar
|
|
258
|
+
that nothing assigned fails the solve, and a pinned `allows_unassigned` scalar
|
|
259
|
+
stays unassigned. It is a declared field, not a callback — there is no dynamic
|
|
260
|
+
`is_pinned()` hook, because the compiled runtime reads a resolved per-row flag.
|
|
261
|
+
The variable-level `planning_variable(pinning=True)` flag stays rejected with
|
|
262
|
+
`ModelValidationError`: SolverForge pins whole entities, and that flag names a
|
|
263
|
+
route the framework does not have.
|
|
251
264
|
- `scalar_assignment_group(...)` declares assignment-aware scalar groups for
|
|
252
265
|
grouped scalar local search and assignment-group construction. Group metadata
|
|
253
266
|
covers required entities, capacity keys, assignment rules, ordering callbacks,
|
|
@@ -20,12 +20,12 @@ requirements, and other APIs that are not part of this package.
|
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
|
-
The package version prepared by this checkout is `solverforge` `0.6.
|
|
23
|
+
The package version prepared by this checkout is `solverforge` `0.6.10`, targeting
|
|
24
24
|
the published and registry-locked SolverForge Rust `0.19.7` dependency base.
|
|
25
25
|
Install the matching package after its Python release with:
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
python3.14 -m pip install "solverforge==0.6.
|
|
28
|
+
python3.14 -m pip install "solverforge==0.6.10"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The installable wheel contains the core `solverforge` package, native extension,
|
|
@@ -45,12 +45,12 @@ make deliveries-run PORT=7861
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
The same source-checkout examples can be run against the matching published
|
|
48
|
-
package once `0.6.
|
|
48
|
+
package once `0.6.10` is released, without `PYTHONPATH` or an editable checkout:
|
|
49
49
|
|
|
50
50
|
```sh
|
|
51
51
|
python3.14 -m venv .venv-examples
|
|
52
52
|
. .venv-examples/bin/activate
|
|
53
|
-
python -m pip install "solverforge[examples]==0.6.
|
|
53
|
+
python -m pip install "solverforge[examples]==0.6.10"
|
|
54
54
|
python examples/nqueens.py
|
|
55
55
|
python -m examples.solverforge_hospital
|
|
56
56
|
python -m examples.solverforge_deliveries
|
|
@@ -73,16 +73,19 @@ from solverforge import (
|
|
|
73
73
|
Solver,
|
|
74
74
|
constraint_provider,
|
|
75
75
|
planning_entity,
|
|
76
|
+
planning_pin,
|
|
76
77
|
planning_solution,
|
|
77
78
|
planning_variable,
|
|
78
79
|
)
|
|
79
80
|
|
|
80
81
|
@planning_entity
|
|
81
82
|
class Shift:
|
|
83
|
+
pinned = planning_pin()
|
|
82
84
|
nurse = planning_variable(value_range_provider="nurses", allows_unassigned=True)
|
|
83
85
|
|
|
84
86
|
def __init__(self, required: bool = True, nurse: int | None = None) -> None:
|
|
85
87
|
self.required = required
|
|
88
|
+
self.pinned = False
|
|
86
89
|
self.nurse = nurse
|
|
87
90
|
|
|
88
91
|
@constraint_provider
|
|
@@ -212,11 +215,21 @@ Run `make help` for focused targets such as `make test-hospital`,
|
|
|
212
215
|
per-query Python callbacks. Provider-backed value ranges are imported once per
|
|
213
216
|
variable and shared across rows in Rust-owned state. Row candidate callbacks
|
|
214
217
|
remain row-specific and define move legality throughout construction and local
|
|
215
|
-
search, including assignment-group swaps and rematches.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
input
|
|
218
|
+
search, including assignment-group swaps and rematches.
|
|
219
|
+
- `planning_pin()` declares the entity attribute that holds an entity's pinned
|
|
220
|
+
flag. The declared attribute must hold a `bool` on every instance; an unset or
|
|
221
|
+
non-`bool` value is rejected at import rather than read as "not pinned". A
|
|
222
|
+
pinned entity keeps its input planning state: its genuine scalar value and the
|
|
223
|
+
elements its list variable owns survive construction, local search,
|
|
224
|
+
ruin/recreate, and exhaustive search, while every unpinned entity in the same
|
|
225
|
+
solve is still constructed and searched. Pinning preserves input state, it does
|
|
226
|
+
not exempt a row from the mandatory-completion gate: a pinned required scalar
|
|
227
|
+
that nothing assigned fails the solve, and a pinned `allows_unassigned` scalar
|
|
228
|
+
stays unassigned. It is a declared field, not a callback — there is no dynamic
|
|
229
|
+
`is_pinned()` hook, because the compiled runtime reads a resolved per-row flag.
|
|
230
|
+
The variable-level `planning_variable(pinning=True)` flag stays rejected with
|
|
231
|
+
`ModelValidationError`: SolverForge pins whole entities, and that flag names a
|
|
232
|
+
route the framework does not have.
|
|
220
233
|
- `scalar_assignment_group(...)` declares assignment-aware scalar groups for
|
|
221
234
|
grouped scalar local search and assignment-group construction. Group metadata
|
|
222
235
|
covers required entities, capacity keys, assignment rules, ordering callbacks,
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "solverforge"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.10"
|
|
8
8
|
description = "Dynamic Python bindings for the SolverForge constraint solver."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -51,6 +51,10 @@ strip = true
|
|
|
51
51
|
testpaths = ["tests/python"]
|
|
52
52
|
pythonpath = ["python"]
|
|
53
53
|
|
|
54
|
+
[tool.black]
|
|
55
|
+
line-length = 100
|
|
56
|
+
target-version = ["py314"]
|
|
57
|
+
|
|
54
58
|
[tool.ruff]
|
|
55
59
|
line-length = 100
|
|
56
60
|
target-version = "py314"
|
|
@@ -28,6 +28,7 @@ from .fields import (
|
|
|
28
28
|
SolutionField,
|
|
29
29
|
planning_id,
|
|
30
30
|
planning_list_variable,
|
|
31
|
+
planning_pin,
|
|
31
32
|
planning_variable,
|
|
32
33
|
)
|
|
33
34
|
from .groups import (
|
|
@@ -40,7 +41,7 @@ from .manager import JobHandle, SolverManager
|
|
|
40
41
|
from .score import HardMediumSoftScore, HardSoftDecimalScore, HardSoftScore, SoftScore
|
|
41
42
|
from .solver import Solver
|
|
42
43
|
|
|
43
|
-
__version__ = "0.6.
|
|
44
|
+
__version__ = "0.6.10"
|
|
44
45
|
|
|
45
46
|
__all__ = [
|
|
46
47
|
"CallbackError",
|
|
@@ -78,6 +79,7 @@ __all__ = [
|
|
|
78
79
|
"planning_entity",
|
|
79
80
|
"planning_id",
|
|
80
81
|
"planning_list_variable",
|
|
82
|
+
"planning_pin",
|
|
81
83
|
"planning_solution",
|
|
82
84
|
"planning_variable",
|
|
83
85
|
"problem_fact",
|
|
@@ -38,18 +38,14 @@ class SolverManager:
|
|
|
38
38
|
def _preflight_qualified_candidate_trace_provenance(
|
|
39
39
|
self,
|
|
40
40
|
*,
|
|
41
|
-
qualified_candidate_trace_provenance:
|
|
42
|
-
QualifiedCandidateTraceProvenance | None
|
|
43
|
-
) = None,
|
|
41
|
+
qualified_candidate_trace_provenance: QualifiedCandidateTraceProvenance | None = None,
|
|
44
42
|
) -> None: ...
|
|
45
43
|
def solve(
|
|
46
44
|
self,
|
|
47
45
|
solution: object,
|
|
48
46
|
schema: CompiledSchema,
|
|
49
47
|
*,
|
|
50
|
-
qualified_candidate_trace_provenance:
|
|
51
|
-
QualifiedCandidateTraceProvenance | None
|
|
52
|
-
) = None,
|
|
48
|
+
qualified_candidate_trace_provenance: QualifiedCandidateTraceProvenance | None = None,
|
|
53
49
|
) -> int: ...
|
|
54
50
|
def get_status(self, job_id: int) -> dict[str, object]: ...
|
|
55
51
|
def telemetry_detail(self, job_id: int) -> dict[str, object]: ...
|
|
@@ -162,9 +162,7 @@ def _normalize_phases(phases: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
162
162
|
dict(phase_copy["termination"])
|
|
163
163
|
).to_dict()
|
|
164
164
|
if "child_phases" in phase_copy:
|
|
165
|
-
phase_copy["child_phases"] = _normalize_phases(
|
|
166
|
-
list(phase_copy["child_phases"])
|
|
167
|
-
)
|
|
165
|
+
phase_copy["child_phases"] = _normalize_phases(list(phase_copy["child_phases"]))
|
|
168
166
|
normalized.append(phase_copy)
|
|
169
167
|
return normalized
|
|
170
168
|
|
|
@@ -163,10 +163,7 @@ class ListPrecedenceMakespanConstraintStream:
|
|
|
163
163
|
if plan.precedence_duration is None and plan.precedence_duration_field is None:
|
|
164
164
|
msg = f"{self.entity_type.__name__}.{self.variable_name} requires precedence_duration"
|
|
165
165
|
raise TypeError(msg)
|
|
166
|
-
if
|
|
167
|
-
plan.precedence_successors is None
|
|
168
|
-
and plan.precedence_successors_field is None
|
|
169
|
-
):
|
|
166
|
+
if plan.precedence_successors is None and plan.precedence_successors_field is None:
|
|
170
167
|
msg = f"{self.entity_type.__name__}.{self.variable_name} requires precedence_successors"
|
|
171
168
|
raise TypeError(msg)
|
|
172
169
|
if plan.element_owner is None and plan.element_owner_field is None:
|
|
@@ -382,9 +379,7 @@ class ConstraintFactory:
|
|
|
382
379
|
self.score_family = score_family
|
|
383
380
|
|
|
384
381
|
def for_each(self, entity_type: type[object]) -> UniConstraintStream:
|
|
385
|
-
return UniConstraintStream(
|
|
386
|
-
entity_type=entity_type, score_family=self.score_family
|
|
387
|
-
)
|
|
382
|
+
return UniConstraintStream(entity_type=entity_type, score_family=self.score_family)
|
|
388
383
|
|
|
389
384
|
def for_each_unassigned_element(
|
|
390
385
|
self,
|
|
@@ -409,14 +404,10 @@ class ConstraintFactory:
|
|
|
409
404
|
)
|
|
410
405
|
|
|
411
406
|
def join(self, *_args: Any, **_kwargs: Any) -> None:
|
|
412
|
-
raise NotImplementedError(
|
|
413
|
-
"dynamic join is implemented in the native stream planner"
|
|
414
|
-
)
|
|
407
|
+
raise NotImplementedError("dynamic join is implemented in the native stream planner")
|
|
415
408
|
|
|
416
409
|
def if_exists(self, *_args: Any, **_kwargs: Any) -> None:
|
|
417
|
-
raise NotImplementedError(
|
|
418
|
-
"dynamic if_exists is implemented in the native stream planner"
|
|
419
|
-
)
|
|
410
|
+
raise NotImplementedError("dynamic if_exists is implemented in the native stream planner")
|
|
420
411
|
|
|
421
412
|
def if_not_exists(self, *_args: Any, **_kwargs: Any) -> None:
|
|
422
413
|
raise NotImplementedError(
|
|
@@ -424,14 +415,10 @@ class ConstraintFactory:
|
|
|
424
415
|
)
|
|
425
416
|
|
|
426
417
|
def group_by(self, *_args: Any, **_kwargs: Any) -> None:
|
|
427
|
-
raise NotImplementedError(
|
|
428
|
-
"dynamic group_by is implemented in the native stream planner"
|
|
429
|
-
)
|
|
418
|
+
raise NotImplementedError("dynamic group_by is implemented in the native stream planner")
|
|
430
419
|
|
|
431
420
|
def flattened(self, *_args: Any, **_kwargs: Any) -> None:
|
|
432
|
-
raise NotImplementedError(
|
|
433
|
-
"dynamic flattened is implemented in the native stream planner"
|
|
434
|
-
)
|
|
421
|
+
raise NotImplementedError("dynamic flattened is implemented in the native stream planner")
|
|
435
422
|
|
|
436
423
|
|
|
437
424
|
def joiner_to_native(joiner: object) -> object:
|
|
@@ -462,9 +449,7 @@ def collector_to_native(collector: object) -> object:
|
|
|
462
449
|
return collector
|
|
463
450
|
|
|
464
451
|
|
|
465
|
-
def _list_variable_element_collection(
|
|
466
|
-
entity_type: type[object], variable_name: str
|
|
467
|
-
) -> str:
|
|
452
|
+
def _list_variable_element_collection(entity_type: type[object], variable_name: str) -> str:
|
|
468
453
|
field_info = _list_variable_metadata(entity_type, variable_name)
|
|
469
454
|
collection = field_info.get("element_collection")
|
|
470
455
|
if isinstance(collection, str) and collection:
|
|
@@ -473,9 +458,7 @@ def _list_variable_element_collection(
|
|
|
473
458
|
raise TypeError(msg)
|
|
474
459
|
|
|
475
460
|
|
|
476
|
-
def _list_variable_metadata(
|
|
477
|
-
entity_type: type[object], variable_name: str
|
|
478
|
-
) -> dict[str, object]:
|
|
461
|
+
def _list_variable_metadata(entity_type: type[object], variable_name: str) -> dict[str, object]:
|
|
479
462
|
metadata = getattr(entity_type, "__solverforge_entity__", None)
|
|
480
463
|
if not isinstance(metadata, dict):
|
|
481
464
|
msg = f"{entity_type.__name__} is not marked with @planning_entity"
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import Any, TypeVar
|
|
5
5
|
|
|
6
|
+
from .errors import ModelValidationError
|
|
6
7
|
from .fields import (
|
|
7
8
|
CapacityRouteFeasibility,
|
|
8
9
|
EntityCallback,
|
|
@@ -22,8 +23,11 @@ T = TypeVar("T", bound=type[object])
|
|
|
22
23
|
|
|
23
24
|
def _collect_fields(cls: type[object]) -> list[dict[str, object]]:
|
|
24
25
|
fields: list[dict[str, object]] = []
|
|
26
|
+
pin_fields: list[str] = []
|
|
25
27
|
for name, value in vars(cls).items():
|
|
26
28
|
if isinstance(value, PlanningField):
|
|
29
|
+
if value.metadata.kind == "planning_pin":
|
|
30
|
+
pin_fields.append(name)
|
|
27
31
|
fields.append(
|
|
28
32
|
{
|
|
29
33
|
"name": name,
|
|
@@ -51,6 +55,9 @@ def _collect_fields(cls: type[object]) -> list[dict[str, object]]:
|
|
|
51
55
|
"list_metadata": _list_metadata(value.metadata.list_metadata),
|
|
52
56
|
}
|
|
53
57
|
)
|
|
58
|
+
if len(pin_fields) > 1:
|
|
59
|
+
msg = f"{cls.__name__} declares more than one planning_pin field"
|
|
60
|
+
raise ModelValidationError(msg)
|
|
54
61
|
return fields
|
|
55
62
|
|
|
56
63
|
|
|
@@ -133,12 +140,18 @@ def planning_entity(cls: T) -> T:
|
|
|
133
140
|
|
|
134
141
|
|
|
135
142
|
def problem_fact(cls: T) -> T:
|
|
143
|
+
fields = _collect_fields(cls)
|
|
144
|
+
if any(field["kind"] == "planning_pin" for field in fields):
|
|
145
|
+
msg = (
|
|
146
|
+
f"{cls.__name__} is a problem fact; planning_pin is only valid on a " "planning entity"
|
|
147
|
+
)
|
|
148
|
+
raise ModelValidationError(msg)
|
|
136
149
|
setattr(
|
|
137
150
|
cls,
|
|
138
151
|
"__solverforge_fact__",
|
|
139
152
|
{
|
|
140
153
|
"type_name": cls.__name__,
|
|
141
|
-
"fields":
|
|
154
|
+
"fields": fields,
|
|
142
155
|
},
|
|
143
156
|
)
|
|
144
157
|
return cls
|
|
@@ -206,9 +219,7 @@ def planning_solution(
|
|
|
206
219
|
shadow_variable_updates: dict[str, Any] | list[dict[str, Any]] | None = None,
|
|
207
220
|
) -> Callable[[T], T]:
|
|
208
221
|
def decorate(cls: T) -> T:
|
|
209
|
-
updates =
|
|
210
|
-
shadow_updates if shadow_updates is not None else shadow_variable_updates
|
|
211
|
-
)
|
|
222
|
+
updates = shadow_updates if shadow_updates is not None else shadow_variable_updates
|
|
212
223
|
if updates is None:
|
|
213
224
|
update_list: list[dict[str, Any]] = []
|
|
214
225
|
elif isinstance(updates, dict):
|
|
@@ -59,9 +59,7 @@ class SolutionCallback:
|
|
|
59
59
|
raise TypeError("SolutionCallback callback must be callable")
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
ListValueSource: TypeAlias =
|
|
63
|
-
RowField | SolutionField | EntityCallback | SolutionCallback
|
|
64
|
-
)
|
|
62
|
+
ListValueSource: TypeAlias = RowField | SolutionField | EntityCallback | SolutionCallback
|
|
65
63
|
ListFeasibilityFieldSource: TypeAlias = RowField | SolutionField
|
|
66
64
|
|
|
67
65
|
|
|
@@ -74,14 +72,10 @@ class CapacityRouteFeasibility:
|
|
|
74
72
|
|
|
75
73
|
def __post_init__(self) -> None:
|
|
76
74
|
if self.capacity is None or self.demand is None:
|
|
77
|
-
raise TypeError(
|
|
78
|
-
"CapacityRouteFeasibility requires capacity and demand sources"
|
|
79
|
-
)
|
|
75
|
+
raise TypeError("CapacityRouteFeasibility requires capacity and demand sources")
|
|
80
76
|
|
|
81
77
|
|
|
82
|
-
ListFeasibilitySource: TypeAlias =
|
|
83
|
-
EntityCallback | SolutionCallback | CapacityRouteFeasibility
|
|
84
|
-
)
|
|
78
|
+
ListFeasibilitySource: TypeAlias = EntityCallback | SolutionCallback | CapacityRouteFeasibility
|
|
85
79
|
|
|
86
80
|
|
|
87
81
|
@dataclass(frozen=True)
|
|
@@ -94,9 +88,7 @@ class ListRouteHooks:
|
|
|
94
88
|
|
|
95
89
|
def __post_init__(self) -> None:
|
|
96
90
|
if self.depot is None or self.distance is None or self.feasible is None:
|
|
97
|
-
raise TypeError(
|
|
98
|
-
"ListRouteHooks requires depot, distance, and feasible sources"
|
|
99
|
-
)
|
|
91
|
+
raise TypeError("ListRouteHooks requires depot, distance, and feasible sources")
|
|
100
92
|
|
|
101
93
|
|
|
102
94
|
@dataclass(frozen=True)
|
|
@@ -181,6 +173,17 @@ def planning_id() -> PlanningField:
|
|
|
181
173
|
return PlanningField(FieldMetadata(kind="planning_id"))
|
|
182
174
|
|
|
183
175
|
|
|
176
|
+
def planning_pin() -> PlanningField:
|
|
177
|
+
"""Declares the entity attribute that holds this entity's pinned flag.
|
|
178
|
+
|
|
179
|
+
The declared attribute must hold a ``bool`` on every instance. A pinned
|
|
180
|
+
entity keeps its input planning state: its genuine scalar value and its
|
|
181
|
+
list ownership survive construction and search.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
return PlanningField(FieldMetadata(kind="planning_pin"))
|
|
185
|
+
|
|
186
|
+
|
|
184
187
|
def planning_variable(
|
|
185
188
|
*,
|
|
186
189
|
value_range_provider: str,
|
|
@@ -195,9 +198,8 @@ def planning_variable(
|
|
|
195
198
|
if pinning:
|
|
196
199
|
msg = (
|
|
197
200
|
"planning_variable(pinning=True) is not supported: SolverForge pins a whole "
|
|
198
|
-
"planning entity
|
|
199
|
-
"
|
|
200
|
-
"value by restricting candidate_values instead."
|
|
201
|
+
"planning entity, not one variable. Declare an entity pin field with "
|
|
202
|
+
"planning_pin() instead."
|
|
201
203
|
)
|
|
202
204
|
raise ModelValidationError(msg)
|
|
203
205
|
value_candidates = _resolve_metadata_source(
|
|
@@ -393,9 +395,7 @@ def _resolve_list_value_source(
|
|
|
393
395
|
solution_arity,
|
|
394
396
|
)
|
|
395
397
|
return source
|
|
396
|
-
raise TypeError(
|
|
397
|
-
f"{name} must be RowField, SolutionField, EntityCallback, or SolutionCallback"
|
|
398
|
-
)
|
|
398
|
+
raise TypeError(f"{name} must be RowField, SolutionField, EntityCallback, or SolutionCallback")
|
|
399
399
|
|
|
400
400
|
|
|
401
401
|
def _resolve_required_list_value_source(
|
|
@@ -445,9 +445,7 @@ def _resolve_list_feasibility_source(
|
|
|
445
445
|
solution_arity,
|
|
446
446
|
)
|
|
447
447
|
return source
|
|
448
|
-
raise TypeError(
|
|
449
|
-
f"{name} must be EntityCallback, SolutionCallback, or CapacityRouteFeasibility"
|
|
450
|
-
)
|
|
448
|
+
raise TypeError(f"{name} must be EntityCallback, SolutionCallback, or CapacityRouteFeasibility")
|
|
451
449
|
|
|
452
450
|
|
|
453
451
|
def _resolve_list_feasibility_field_source(
|
|
@@ -491,10 +489,7 @@ def _callback_accepts_arity(callback: Callable[..., object], arity: int) -> bool
|
|
|
491
489
|
required_positional_count += 1
|
|
492
490
|
elif parameter.kind is Parameter.VAR_POSITIONAL:
|
|
493
491
|
accepts_extra_positionals = True
|
|
494
|
-
elif
|
|
495
|
-
parameter.kind is Parameter.KEYWORD_ONLY
|
|
496
|
-
and parameter.default is Parameter.empty
|
|
497
|
-
):
|
|
492
|
+
elif parameter.kind is Parameter.KEYWORD_ONLY and parameter.default is Parameter.empty:
|
|
498
493
|
return False
|
|
499
494
|
|
|
500
495
|
if arity < required_positional_count:
|
|
@@ -89,17 +89,10 @@ class ScalarAssignmentGroup:
|
|
|
89
89
|
),
|
|
90
90
|
):
|
|
91
91
|
if callback is not None and field is not None:
|
|
92
|
-
raise TypeError(
|
|
93
|
-
f"{callback_name} and {field_name} cannot both be configured"
|
|
94
|
-
)
|
|
92
|
+
raise TypeError(f"{callback_name} and {field_name} cannot both be configured")
|
|
95
93
|
if field is not None and (not isinstance(field, str) or not field):
|
|
96
|
-
raise TypeError(
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
if (
|
|
100
|
-
self.assignment_rule is not None
|
|
101
|
-
and self.same_value_conflict_field is not None
|
|
102
|
-
):
|
|
94
|
+
raise TypeError(f"{field_name} must be a non-empty string when provided")
|
|
95
|
+
if self.assignment_rule is not None and self.same_value_conflict_field is not None:
|
|
103
96
|
raise TypeError(
|
|
104
97
|
"assignment_rule and same_value_conflict_field cannot both be configured"
|
|
105
98
|
)
|
|
@@ -107,9 +100,7 @@ class ScalarAssignmentGroup:
|
|
|
107
100
|
not isinstance(self.same_value_conflict_field, str)
|
|
108
101
|
or not self.same_value_conflict_field
|
|
109
102
|
):
|
|
110
|
-
raise TypeError(
|
|
111
|
-
"same_value_conflict_field must be a non-empty string when provided"
|
|
112
|
-
)
|
|
103
|
+
raise TypeError("same_value_conflict_field must be a non-empty string when provided")
|
|
113
104
|
|
|
114
105
|
def to_native(self) -> dict[str, Any]:
|
|
115
106
|
return {
|
|
@@ -90,9 +90,7 @@ def _schema_shape(value: object) -> tuple[SchemaShape, bool]:
|
|
|
90
90
|
|
|
91
91
|
def shape(item: object, *, callback_state: bool = False) -> SchemaShape:
|
|
92
92
|
nonlocal cacheable, next_marker
|
|
93
|
-
recursive = isinstance(
|
|
94
|
-
item, (dict, list, tuple, FunctionType, MethodType, partial)
|
|
95
|
-
)
|
|
93
|
+
recursive = isinstance(item, (dict, list, tuple, FunctionType, MethodType, partial))
|
|
96
94
|
item_id = id(item)
|
|
97
95
|
if recursive:
|
|
98
96
|
if item_id in active:
|
|
@@ -147,8 +145,7 @@ def _schema_shape(value: object) -> tuple[SchemaShape, bool]:
|
|
|
147
145
|
for cell in (item.__closure__ or ())
|
|
148
146
|
)
|
|
149
147
|
defaults = tuple(
|
|
150
|
-
shape(default, callback_state=True)
|
|
151
|
-
for default in (item.__defaults__ or ())
|
|
148
|
+
shape(default, callback_state=True) for default in (item.__defaults__ or ())
|
|
152
149
|
)
|
|
153
150
|
keyword_defaults = tuple(
|
|
154
151
|
(name, shape(default, callback_state=True))
|
|
@@ -241,7 +238,9 @@ def _scalar_groups(callbacks: object) -> list[dict[str, object]]:
|
|
|
241
238
|
callback = cast(Callable[..., object], item)
|
|
242
239
|
metadata = getattr(callback, "__solverforge_scalar_group__", None)
|
|
243
240
|
if not isinstance(metadata, dict):
|
|
244
|
-
msg =
|
|
241
|
+
msg = (
|
|
242
|
+
f"{callback!r} is not marked with @scalar_group and is not a ScalarAssignmentGroup"
|
|
243
|
+
)
|
|
245
244
|
raise ModelValidationError(msg)
|
|
246
245
|
name = metadata.get("name")
|
|
247
246
|
if not isinstance(name, str) or not name:
|
|
@@ -262,9 +261,7 @@ def _conflict_repairs(callbacks: object) -> list[dict[str, object]]:
|
|
|
262
261
|
if not isinstance(metadata, dict):
|
|
263
262
|
msg = f"{callback!r} is not marked with @conflict_repair"
|
|
264
263
|
raise ModelValidationError(msg)
|
|
265
|
-
constraint_names = list(
|
|
266
|
-
cast(Iterable[object], metadata.get("constraints") or [])
|
|
267
|
-
)
|
|
264
|
+
constraint_names = list(cast(Iterable[object], metadata.get("constraints") or []))
|
|
268
265
|
if not constraint_names or not all(
|
|
269
266
|
isinstance(name, str) and name for name in constraint_names
|
|
270
267
|
):
|
|
@@ -9,9 +9,7 @@ from .model import _compiled_schema_for_solution
|
|
|
9
9
|
|
|
10
10
|
class Solver:
|
|
11
11
|
@staticmethod
|
|
12
|
-
def solve(
|
|
13
|
-
solution: object, config: SolverConfig | dict[str, Any] | None = None
|
|
14
|
-
) -> object:
|
|
12
|
+
def solve(solution: object, config: SolverConfig | dict[str, Any] | None = None) -> object:
|
|
15
13
|
schema = _compiled_schema_for_solution(solution)
|
|
16
14
|
return _native.solve(solution, schema, _resolve_config(config))
|
|
17
15
|
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
use std::any::TypeId;
|
|
1
|
+
use std::any::{Any, TypeId};
|
|
2
2
|
|
|
3
3
|
use solverforge_bridge::{EntityClassId, VariableId};
|
|
4
4
|
use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, VariableDescriptor};
|
|
5
5
|
|
|
6
6
|
use crate::descriptor::extractor::DynamicEntityExtractor;
|
|
7
7
|
use crate::intern::intern;
|
|
8
|
+
use crate::state::entity_table::DynamicEntityRow;
|
|
8
9
|
use crate::state::PyDynamicSolution;
|
|
9
10
|
|
|
10
11
|
use super::DynamicSchema;
|
|
11
12
|
|
|
13
|
+
/// The one entity pin predicate for every compiled Python schema.
|
|
14
|
+
///
|
|
15
|
+
/// The declaration is resolved per row at import, so this stays a capture-free
|
|
16
|
+
/// function over Rust-owned state: no thread-local slot lookup, no Python
|
|
17
|
+
/// callback, and no per-candidate reinterpretation of the declared attribute.
|
|
18
|
+
fn pinned_row_predicate(entity: &dyn Any) -> bool {
|
|
19
|
+
entity
|
|
20
|
+
.downcast_ref::<DynamicEntityRow>()
|
|
21
|
+
.is_some_and(|row| row.pinned)
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
pub fn solution_descriptor(schema: &DynamicSchema) -> SolutionDescriptor {
|
|
13
25
|
let mut descriptor = SolutionDescriptor::new(
|
|
14
26
|
intern(schema.solution_type.clone()),
|
|
@@ -37,6 +49,11 @@ pub fn solution_descriptor(schema: &DynamicSchema) -> SolutionDescriptor {
|
|
|
37
49
|
.with_logical_id(VariableId(variable_index));
|
|
38
50
|
entity_descriptor = entity_descriptor.with_variable(descriptor_variable);
|
|
39
51
|
}
|
|
52
|
+
if let Some(pin_field) = entity.pin_field.as_deref() {
|
|
53
|
+
entity_descriptor = entity_descriptor
|
|
54
|
+
.with_pin_field(intern(pin_field.to_string()))
|
|
55
|
+
.with_pin_predicate(pinned_row_predicate);
|
|
56
|
+
}
|
|
40
57
|
descriptor = descriptor.with_entity(entity_descriptor);
|
|
41
58
|
}
|
|
42
59
|
descriptor
|
|
@@ -34,9 +34,20 @@ pub fn parse_schema(schema: &Bound<'_, PyDict>) -> PyResult<DynamicSchema> {
|
|
|
34
34
|
.ok_or_else(|| py_err(format!("entity `{type_name}` is missing `fields`")))?;
|
|
35
35
|
let fields = fields_any.cast::<PyList>()?;
|
|
36
36
|
let mut variables = Vec::new();
|
|
37
|
+
let mut pin_field: Option<String> = None;
|
|
37
38
|
for field_any in fields.iter() {
|
|
38
39
|
let field = field_any.cast::<PyDict>()?;
|
|
39
40
|
let kind = required_str(field, "kind")?;
|
|
41
|
+
if kind == "planning_pin" {
|
|
42
|
+
let name = required_non_empty_str(field, "name")?;
|
|
43
|
+
if pin_field.is_some() {
|
|
44
|
+
return Err(py_err(format!(
|
|
45
|
+
"entity `{type_name}` declares more than one planning_pin field"
|
|
46
|
+
)));
|
|
47
|
+
}
|
|
48
|
+
pin_field = Some(name);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
40
51
|
if kind == "planning_variable" || kind == "planning_list_variable" {
|
|
41
52
|
let list_metadata = if kind == "planning_list_variable" {
|
|
42
53
|
Some(parse_list_metadata(field)?)
|
|
@@ -98,10 +109,19 @@ pub fn parse_schema(schema: &Bound<'_, PyDict>) -> PyResult<DynamicSchema> {
|
|
|
98
109
|
});
|
|
99
110
|
}
|
|
100
111
|
}
|
|
112
|
+
if let Some(pin_field) = pin_field.as_deref() {
|
|
113
|
+
if variables.iter().any(|variable| variable.name == pin_field) {
|
|
114
|
+
return Err(py_err(format!(
|
|
115
|
+
"entity `{type_name}` declares `{pin_field}` as both a planning \
|
|
116
|
+
variable and a planning_pin field"
|
|
117
|
+
)));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
101
120
|
entities.push(EntitySchema {
|
|
102
121
|
type_name,
|
|
103
122
|
collection,
|
|
104
123
|
variables,
|
|
124
|
+
pin_field,
|
|
105
125
|
});
|
|
106
126
|
}
|
|
107
127
|
let facts = parse_facts(schema)?;
|
|
@@ -7,6 +7,10 @@ pub struct EntitySchema {
|
|
|
7
7
|
pub type_name: String,
|
|
8
8
|
pub collection: String,
|
|
9
9
|
pub variables: Vec<VariableSchema>,
|
|
10
|
+
/// The declared entity attribute holding this entity's pinned flag. It is
|
|
11
|
+
/// read once per instance at import and enforced by the compiled
|
|
12
|
+
/// SolverForge runtime, never by wrapper-owned phase code.
|
|
13
|
+
pub pin_field: Option<String>,
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
#[derive(Debug)]
|
|
@@ -76,6 +76,11 @@ pub struct DynamicEntityRow {
|
|
|
76
76
|
pub scalar_values: Vec<Option<usize>>,
|
|
77
77
|
pub candidate_values: Vec<Option<Arc<[usize]>>>,
|
|
78
78
|
pub list_values: Vec<Option<Vec<usize>>>,
|
|
79
|
+
/// The entity's declared pinned flag, resolved once at import. It is the
|
|
80
|
+
/// only input the compiled runtime reads through the descriptor's pin
|
|
81
|
+
/// predicate. It is a plain row field, so every copy of a row that clones
|
|
82
|
+
/// (candidate, snapshot, or detached solution) carries the resolved value.
|
|
83
|
+
pub pinned: bool,
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
impl DynamicEntityRow {
|
|
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|
|
2
2
|
use std::sync::Arc;
|
|
3
3
|
|
|
4
4
|
use pyo3::prelude::*;
|
|
5
|
-
use pyo3::types::{PyDict, PyInt, PyList, PyString};
|
|
5
|
+
use pyo3::types::{PyBool, PyDict, PyInt, PyList, PyString};
|
|
6
6
|
use solverforge_config::SolverConfig;
|
|
7
7
|
|
|
8
8
|
use crate::schema::runtime_plan::CompiledRuntimePlan;
|
|
@@ -116,6 +116,9 @@ pub fn import_solution(
|
|
|
116
116
|
Some(&row_metadata_fields),
|
|
117
117
|
)?;
|
|
118
118
|
import_named_fields(&item, &extra_field_names, &mut row)?;
|
|
119
|
+
if let Some(pin_field) = entity.pin_field.as_deref() {
|
|
120
|
+
row.pinned = import_pin_value(&item, pin_field, &entity.type_name)?;
|
|
121
|
+
}
|
|
119
122
|
rows.push(row);
|
|
120
123
|
objects.push(item.unbind());
|
|
121
124
|
}
|
|
@@ -1176,6 +1179,40 @@ fn import_named_fields(
|
|
|
1176
1179
|
Ok(())
|
|
1177
1180
|
}
|
|
1178
1181
|
|
|
1182
|
+
/// Reads one entity's declared pinned flag.
|
|
1183
|
+
///
|
|
1184
|
+
/// The declaration is a contract on the instance, not a hint: an unset, missing,
|
|
1185
|
+
/// or non-`bool` value is an error rather than an implicit "not pinned", because
|
|
1186
|
+
/// a silently unpinned declaration would look identical to a working one while
|
|
1187
|
+
/// preserving nothing. The declared attribute is a `planning_pin()` descriptor,
|
|
1188
|
+
/// so an instance that never sets it reads back as `None` rather than raising.
|
|
1189
|
+
fn import_pin_value(item: &Bound<'_, PyAny>, pin_field: &str, type_name: &str) -> PyResult<bool> {
|
|
1190
|
+
let value = match item.getattr(pin_field) {
|
|
1191
|
+
Ok(value) => value,
|
|
1192
|
+
Err(cause) => {
|
|
1193
|
+
// Keep the original failure as the cause: a declared attribute that
|
|
1194
|
+
// exists but raises is a different problem from one that is absent,
|
|
1195
|
+
// and the operator needs the original traceback to tell them apart.
|
|
1196
|
+
let error = crate::error::py_err(format!(
|
|
1197
|
+
"pinned entity `{type_name}` has no readable `{pin_field}` attribute"
|
|
1198
|
+
));
|
|
1199
|
+
error.set_cause(item.py(), Some(cause));
|
|
1200
|
+
return Err(error);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
if value.is_none() {
|
|
1204
|
+
return Err(crate::error::py_err(format!(
|
|
1205
|
+
"pinned entity `{type_name}` must set a bool `{pin_field}` attribute per instance"
|
|
1206
|
+
)));
|
|
1207
|
+
}
|
|
1208
|
+
if !value.is_instance_of::<PyBool>() {
|
|
1209
|
+
return Err(crate::error::py_err(format!(
|
|
1210
|
+
"pinned entity `{type_name}` attribute `{pin_field}` must be a bool"
|
|
1211
|
+
)));
|
|
1212
|
+
}
|
|
1213
|
+
value.extract::<bool>()
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1179
1216
|
fn is_read_only_property(
|
|
1180
1217
|
item: &Bound<'_, PyAny>,
|
|
1181
1218
|
name: &str,
|
|
@@ -741,6 +741,7 @@ mod tests {
|
|
|
741
741
|
entities: vec![EntitySchema {
|
|
742
742
|
type_name: "Route".to_string(),
|
|
743
743
|
collection: "routes".to_string(),
|
|
744
|
+
pin_field: None,
|
|
744
745
|
variables: vec![VariableSchema {
|
|
745
746
|
name: "visits".to_string(),
|
|
746
747
|
storage_name: "__solverforge_visits".to_string(),
|
|
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
|