plansolve 0.25.0__py3-none-any.whl
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.
- plansolve/__init__.py +66 -0
- plansolve/client.py +40 -0
- plansolve/field_service/__init__.py +34 -0
- plansolve/field_service/api_client.py +212 -0
- plansolve/field_service/constraint_weight.py +143 -0
- plansolve/field_service/models.py +427 -0
- plansolve/professional_services/__init__.py +28 -0
- plansolve/professional_services/api_client.py +209 -0
- plansolve/professional_services/availability_time_span.py +31 -0
- plansolve/professional_services/contract.py +39 -0
- plansolve/professional_services/models.py +265 -0
- plansolve/py.typed +4 -0
- plansolve/routes.py +8 -0
- plansolve/shift/__init__.py +45 -0
- plansolve/shift/api_client.py +210 -0
- plansolve/shift/constraint_weight_overrides.py +19 -0
- plansolve/shift/contract.py +62 -0
- plansolve/shift/day_off_request.py +30 -0
- plansolve/shift/fairness.py +30 -0
- plansolve/shift/fairness_bucket.py +38 -0
- plansolve/shift/models.py +140 -0
- plansolve/shift/options.py +34 -0
- plansolve/shift/period_rule.py +42 -0
- plansolve/shift/planning_period.py +23 -0
- plansolve/shift/shift_assignment.py +59 -0
- plansolve/shift/shift_employee.py +69 -0
- plansolve/shift/shift_off_request.py +30 -0
- plansolve/shift/weights.py +94 -0
- plansolve/solver_status.py +23 -0
- plansolve-0.25.0.dist-info/METADATA +788 -0
- plansolve-0.25.0.dist-info/RECORD +34 -0
- plansolve-0.25.0.dist-info/WHEEL +5 -0
- plansolve-0.25.0.dist-info/licenses/LICENSE +24 -0
- plansolve-0.25.0.dist-info/top_level.txt +1 -0
plansolve/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlanSolve Python SDK
|
|
3
|
+
|
|
4
|
+
A Python client library for the PlanSolve optimization API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import PlanSolveClient
|
|
8
|
+
from .field_service.constraint_weight import ConstraintWeight
|
|
9
|
+
from .field_service.api_client import FieldServiceApiClient
|
|
10
|
+
from .professional_services.api_client import ProfessionalServicesApiClient
|
|
11
|
+
|
|
12
|
+
# Field Service Models
|
|
13
|
+
from .field_service.models import (
|
|
14
|
+
Shift,
|
|
15
|
+
TimeWindow,
|
|
16
|
+
Vehicle,
|
|
17
|
+
Visit,
|
|
18
|
+
SolverOptions,
|
|
19
|
+
Weights,
|
|
20
|
+
FieldServiceRequest,
|
|
21
|
+
ScheduledVehicle,
|
|
22
|
+
ScheduledVisit,
|
|
23
|
+
FieldServiceResponse,
|
|
24
|
+
FieldServiceResultResponse,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Professional Services Models
|
|
28
|
+
from .professional_services.models import (
|
|
29
|
+
Employee,
|
|
30
|
+
Task,
|
|
31
|
+
ProfessionalServicesRequest,
|
|
32
|
+
ScheduledEmployee,
|
|
33
|
+
ScheduledTask,
|
|
34
|
+
ProfessionalServicesResponse,
|
|
35
|
+
ProfessionalServicesResultResponse,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__version__ = "0.25.0"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"PlanSolveClient",
|
|
42
|
+
"FieldServiceApiClient",
|
|
43
|
+
"ProfessionalServicesApiClient",
|
|
44
|
+
"ConstraintWeight",
|
|
45
|
+
# Field Service Models
|
|
46
|
+
"Shift",
|
|
47
|
+
"TimeWindow",
|
|
48
|
+
"Vehicle",
|
|
49
|
+
"Visit",
|
|
50
|
+
"SolverOptions",
|
|
51
|
+
"Weights",
|
|
52
|
+
"FieldServiceRequest",
|
|
53
|
+
"ScheduledVehicle",
|
|
54
|
+
"ScheduledVisit",
|
|
55
|
+
"FieldServiceResponse",
|
|
56
|
+
"FieldServiceResultResponse",
|
|
57
|
+
# Professional Services Models
|
|
58
|
+
"Employee",
|
|
59
|
+
"Task",
|
|
60
|
+
"ProfessionalServicesRequest",
|
|
61
|
+
"ScheduledEmployee",
|
|
62
|
+
"ScheduledTask",
|
|
63
|
+
"ProfessionalServicesResponse",
|
|
64
|
+
"ProfessionalServicesResultResponse",
|
|
65
|
+
]
|
|
66
|
+
|
plansolve/client.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Main PlanSolve client class."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .field_service.api_client import FieldServiceApiClient
|
|
6
|
+
from .professional_services.api_client import ProfessionalServicesApiClient
|
|
7
|
+
from .shift.api_client import ShiftApiClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PlanSolveClient:
|
|
11
|
+
"""Main client class for interacting with the PlanSolve API."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the PlanSolve client.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
api_key: Optional API key for authenticated requests
|
|
19
|
+
"""
|
|
20
|
+
self.field_service = FieldServiceApiClient(api_key)
|
|
21
|
+
self.professional_services = ProfessionalServicesApiClient(api_key)
|
|
22
|
+
self.shift = ShiftApiClient(api_key)
|
|
23
|
+
|
|
24
|
+
def get_status(self, job_id: str) -> dict:
|
|
25
|
+
"""
|
|
26
|
+
Get the status of a running optimization job.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
job_id: The job ID to check status for
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Status response dictionary
|
|
33
|
+
|
|
34
|
+
Note:
|
|
35
|
+
This method uses the field service endpoint for status checking.
|
|
36
|
+
For professional services, use professional_services.get_status() instead.
|
|
37
|
+
"""
|
|
38
|
+
return self.field_service.get_status(job_id)
|
|
39
|
+
|
|
40
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Field service optimization module."""
|
|
2
|
+
|
|
3
|
+
from .api_client import FieldServiceApiClient
|
|
4
|
+
from .constraint_weight import ConstraintWeight
|
|
5
|
+
from .models import (
|
|
6
|
+
Shift,
|
|
7
|
+
TimeWindow,
|
|
8
|
+
Vehicle,
|
|
9
|
+
Visit,
|
|
10
|
+
SolverOptions,
|
|
11
|
+
Weights,
|
|
12
|
+
FieldServiceRequest,
|
|
13
|
+
ScheduledVehicle,
|
|
14
|
+
ScheduledVisit,
|
|
15
|
+
FieldServiceResponse,
|
|
16
|
+
FieldServiceResultResponse,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"FieldServiceApiClient",
|
|
21
|
+
"ConstraintWeight",
|
|
22
|
+
"Shift",
|
|
23
|
+
"TimeWindow",
|
|
24
|
+
"Vehicle",
|
|
25
|
+
"Visit",
|
|
26
|
+
"SolverOptions",
|
|
27
|
+
"Weights",
|
|
28
|
+
"FieldServiceRequest",
|
|
29
|
+
"ScheduledVehicle",
|
|
30
|
+
"ScheduledVisit",
|
|
31
|
+
"FieldServiceResponse",
|
|
32
|
+
"FieldServiceResultResponse",
|
|
33
|
+
]
|
|
34
|
+
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Field service API client."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from ..routes import Routes
|
|
9
|
+
from ..solver_status import SOLVER_STATUS, SolverStatusResponse
|
|
10
|
+
from .models import (
|
|
11
|
+
FieldServiceRequest,
|
|
12
|
+
FieldServiceResponse,
|
|
13
|
+
FieldServiceResultResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FieldServiceApiClient:
|
|
18
|
+
"""Client for field service optimization API."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the field service API client.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
api_key: Optional API key for authenticated requests
|
|
26
|
+
"""
|
|
27
|
+
self.base_url = f"https://plansolve.app{Routes.FIELD_SERVICE_SOLVE}"
|
|
28
|
+
self.api_key = api_key
|
|
29
|
+
|
|
30
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
31
|
+
"""Get request headers with optional API key."""
|
|
32
|
+
headers = {"Content-Type": "application/json"}
|
|
33
|
+
if self.api_key:
|
|
34
|
+
headers["X-API-KEY"] = self.api_key
|
|
35
|
+
return headers
|
|
36
|
+
|
|
37
|
+
def start(
|
|
38
|
+
self, request: Union[FieldServiceRequest, Dict[str, Any]]
|
|
39
|
+
) -> FieldServiceResponse:
|
|
40
|
+
"""
|
|
41
|
+
Start a new field service optimization.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
request: Field service request (FieldServiceRequest dataclass or dict)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
FieldServiceResponse with jobId, solverJobId, result, and error
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
requests.RequestException: If the API request fails
|
|
51
|
+
"""
|
|
52
|
+
# Convert dataclass to dict if needed
|
|
53
|
+
if isinstance(request, FieldServiceRequest):
|
|
54
|
+
api_request = request.to_dict()
|
|
55
|
+
else:
|
|
56
|
+
api_request = request
|
|
57
|
+
|
|
58
|
+
response = requests.post(
|
|
59
|
+
self.base_url, json=api_request, headers=self._get_headers()
|
|
60
|
+
)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
return FieldServiceResponse.from_dict(response.json())
|
|
63
|
+
|
|
64
|
+
def get_result(self, job_id: str) -> FieldServiceResultResponse:
|
|
65
|
+
"""
|
|
66
|
+
Get the completed optimization result.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
job_id: The job ID to get results for
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
FieldServiceResultResponse with vehicles, visits, score, etc.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
requests.RequestException: If the API request fails
|
|
76
|
+
"""
|
|
77
|
+
url = f"{self.base_url}/{job_id}"
|
|
78
|
+
headers = {}
|
|
79
|
+
if self.api_key:
|
|
80
|
+
headers["X-API-KEY"] = self.api_key
|
|
81
|
+
response = requests.get(url, headers=headers)
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
return FieldServiceResultResponse.from_dict(response.json())
|
|
84
|
+
|
|
85
|
+
def get_status(self, job_id: str) -> SolverStatusResponse:
|
|
86
|
+
"""
|
|
87
|
+
Get the status of a running optimization job.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
job_id: The job ID to check status for
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Status response dictionary
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
requests.RequestException: If the API request fails
|
|
97
|
+
"""
|
|
98
|
+
url = f"{self.base_url}/{job_id}/status"
|
|
99
|
+
headers = {}
|
|
100
|
+
if self.api_key:
|
|
101
|
+
headers["X-API-KEY"] = self.api_key
|
|
102
|
+
response = requests.get(url, headers=headers)
|
|
103
|
+
response.raise_for_status()
|
|
104
|
+
return response.json()
|
|
105
|
+
|
|
106
|
+
def analyze(self, job_id: str) -> Dict[str, Any]:
|
|
107
|
+
"""
|
|
108
|
+
Get analysis for a completed optimization job.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
job_id: The job ID to get analysis for
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Analysis response dictionary
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
requests.RequestException: If the API request fails
|
|
118
|
+
"""
|
|
119
|
+
url = f"{self.base_url}/{job_id}/analyze"
|
|
120
|
+
headers = {}
|
|
121
|
+
if self.api_key:
|
|
122
|
+
headers["X-API-KEY"] = self.api_key
|
|
123
|
+
response = requests.get(url, headers=headers)
|
|
124
|
+
response.raise_for_status()
|
|
125
|
+
return response.json()
|
|
126
|
+
|
|
127
|
+
def start_and_wait_for_completion(
|
|
128
|
+
self,
|
|
129
|
+
request: Union[FieldServiceRequest, Dict[str, Any]],
|
|
130
|
+
poll_interval_ms: int = 5000,
|
|
131
|
+
max_attempts: int = 10,
|
|
132
|
+
) -> FieldServiceResultResponse:
|
|
133
|
+
"""
|
|
134
|
+
Start a job and wait for completion, polling for status.
|
|
135
|
+
Matches the logic used in the .NET SDK implementation exactly.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
request: Field service request (FieldServiceRequest dataclass or dict)
|
|
139
|
+
poll_interval_ms: Milliseconds to wait between status checks
|
|
140
|
+
max_attempts: Maximum number of polling attempts
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
FieldServiceResultResponse with optimization results
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If jobId is not returned or solver times out
|
|
147
|
+
requests.RequestException: If API requests fail
|
|
148
|
+
"""
|
|
149
|
+
start_response = self.start(request)
|
|
150
|
+
return self.wait_for_completion(start_response.job_id, poll_interval_ms, max_attempts)
|
|
151
|
+
|
|
152
|
+
def wait_for_completion(
|
|
153
|
+
self,
|
|
154
|
+
job_id: str,
|
|
155
|
+
poll_interval_ms: int = 5000,
|
|
156
|
+
max_attempts: int = 10,
|
|
157
|
+
) -> FieldServiceResultResponse:
|
|
158
|
+
"""
|
|
159
|
+
Wait for a job to complete, polling for status.
|
|
160
|
+
Matches the logic used in the .NET SDK implementation exactly.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
job_id: The job ID to wait for
|
|
164
|
+
poll_interval_ms: Milliseconds to wait between status checks
|
|
165
|
+
max_attempts: Maximum number of polling attempts
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
FieldServiceResultResponse with optimization results
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
ValueError: If jobId is not provided or solver times out
|
|
172
|
+
requests.RequestException: If API requests fail
|
|
173
|
+
"""
|
|
174
|
+
if not job_id:
|
|
175
|
+
raise ValueError("JobId was not returned from wait_for_completion.")
|
|
176
|
+
|
|
177
|
+
attempts = 0
|
|
178
|
+
status: SolverStatusResponse
|
|
179
|
+
|
|
180
|
+
while True:
|
|
181
|
+
time.sleep(poll_interval_ms / 1000.0) # Convert ms to seconds
|
|
182
|
+
status = self.get_status(job_id)
|
|
183
|
+
attempts += 1
|
|
184
|
+
|
|
185
|
+
if not self._is_still_solving(status) or attempts >= max_attempts:
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
if self._is_still_solving(status):
|
|
189
|
+
raise ValueError("Solver did not finish in the allotted time.")
|
|
190
|
+
|
|
191
|
+
return self.get_result(job_id)
|
|
192
|
+
|
|
193
|
+
def _is_still_solving(self, status: SolverStatusResponse) -> bool:
|
|
194
|
+
"""
|
|
195
|
+
Determine if the solver is still running based on the status response.
|
|
196
|
+
Matches the logic used in the .NET SDK implementation exactly.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
status: Status response dictionary
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
True if still solving, False otherwise
|
|
203
|
+
"""
|
|
204
|
+
# Match .NET logic: status.Solving || status.SolverStatus == SolverStatus.SOLVING_SCHEDULED ||
|
|
205
|
+
# (status.SolverStatus != SolverStatus.NOT_SOLVING) || string.IsNullOrEmpty(status.Score)
|
|
206
|
+
return (
|
|
207
|
+
status.get("solving", False)
|
|
208
|
+
or status.get("solverStatus") == SOLVER_STATUS["SOLVING_SCHEDULED"]
|
|
209
|
+
or status.get("solverStatus") != SOLVER_STATUS["NOT_SOLVING"]
|
|
210
|
+
or not status.get("score")
|
|
211
|
+
)
|
|
212
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Constraint weight representation for field service optimization."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConstraintWeight:
|
|
8
|
+
"""
|
|
9
|
+
Represents a constraint weight in the format "Xhard/Ymedium/Zsoft" where X, Y, Z are numeric values.
|
|
10
|
+
Only one of hard, medium, or soft can be non-zero at a time.
|
|
11
|
+
Examples: "1hard/0medium/0soft", "0hard/1medium/0soft", "0hard/0medium/1soft"
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, hard: int, medium: int, soft: int):
|
|
15
|
+
"""
|
|
16
|
+
Create a new ConstraintWeight with the specified values.
|
|
17
|
+
Only one of hard, medium, or soft can be non-zero.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
hard: Hard constraint weight value
|
|
21
|
+
medium: Medium constraint weight value
|
|
22
|
+
soft: Soft constraint weight value
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: When more than one value is non-zero
|
|
26
|
+
"""
|
|
27
|
+
non_zero_count = sum(1 for x in [hard, medium, soft] if x != 0)
|
|
28
|
+
if non_zero_count > 1:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Only one of hard, medium, or soft can be non-zero. "
|
|
31
|
+
f"Received: hard={hard}, medium={medium}, soft={soft}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self.hard = hard
|
|
35
|
+
self.medium = medium
|
|
36
|
+
self.soft = soft
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def parse(value: str) -> "ConstraintWeight":
|
|
40
|
+
"""
|
|
41
|
+
Parse a weight string in the format "Xhard/Ymedium/Zsoft".
|
|
42
|
+
Only one of X, Y, or Z can be non-zero.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
value: The weight string to parse
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
A ConstraintWeight instance
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ValueError: When the string format is invalid or more than one value is non-zero
|
|
52
|
+
"""
|
|
53
|
+
if not value or not value.strip():
|
|
54
|
+
raise ValueError("Weight value cannot be null or empty")
|
|
55
|
+
|
|
56
|
+
# Pattern: (\d+)hard/(\d+)medium/(\d+)soft
|
|
57
|
+
pattern = r"^(\d+)hard/(\d+)medium/(\d+)soft$"
|
|
58
|
+
match = re.match(pattern, value, re.IGNORECASE)
|
|
59
|
+
|
|
60
|
+
if not match:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Invalid weight format: '{value}'. Expected format: 'Xhard/Ymedium/Zsoft'"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
hard = int(match.group(1))
|
|
66
|
+
medium = int(match.group(2))
|
|
67
|
+
soft = int(match.group(3))
|
|
68
|
+
|
|
69
|
+
return ConstraintWeight(hard, medium, soft)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def try_parse(value: Optional[str]) -> Optional["ConstraintWeight"]:
|
|
73
|
+
"""
|
|
74
|
+
Attempt to parse a weight string. Returns the ConstraintWeight if successful, None otherwise.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
value: The weight string to parse, or None
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
ConstraintWeight instance if successful, None otherwise
|
|
81
|
+
"""
|
|
82
|
+
if not value or not value.strip():
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
return ConstraintWeight.parse(value)
|
|
87
|
+
except (ValueError, AttributeError):
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def __str__(self) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Return the string representation in the format "Xhard/Ymedium/Zsoft".
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
String representation of the weight
|
|
96
|
+
"""
|
|
97
|
+
return f"{self.hard}hard/{self.medium}medium/{self.soft}soft"
|
|
98
|
+
|
|
99
|
+
def __repr__(self) -> str:
|
|
100
|
+
"""Return a string representation of the ConstraintWeight."""
|
|
101
|
+
return f"ConstraintWeight(hard={self.hard}, medium={self.medium}, soft={self.soft})"
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def create_hard(value: int) -> "ConstraintWeight":
|
|
105
|
+
"""
|
|
106
|
+
Create a hard constraint weight (Xhard/0medium/0soft).
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
value: The hard constraint value
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
ConstraintWeight instance
|
|
113
|
+
"""
|
|
114
|
+
return ConstraintWeight(value, 0, 0)
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def create_medium(value: int) -> "ConstraintWeight":
|
|
118
|
+
"""
|
|
119
|
+
Create a medium constraint weight (0hard/Xmedium/0soft).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
value: The medium constraint value
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
ConstraintWeight instance
|
|
126
|
+
"""
|
|
127
|
+
return ConstraintWeight(0, value, 0)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def create_soft(value: int) -> "ConstraintWeight":
|
|
131
|
+
"""
|
|
132
|
+
Create a soft constraint weight (0hard/0medium/Xsoft).
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
value: The soft constraint value
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
ConstraintWeight instance
|
|
139
|
+
"""
|
|
140
|
+
return ConstraintWeight(0, 0, value)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|