trismik 0.9.0__py3-none-any.whl → 0.9.4__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.
- trismik/__init__.py +10 -20
- trismik/_mapper.py +335 -79
- trismik/_utils.py +121 -44
- trismik/adaptive_test.py +617 -0
- trismik/client_async.py +362 -280
- trismik/exceptions.py +57 -6
- trismik/settings.py +15 -0
- trismik/types.py +301 -133
- {trismik-0.9.0.dist-info → trismik-0.9.4.dist-info}/LICENSE +21 -21
- trismik-0.9.4.dist-info/METADATA +172 -0
- trismik-0.9.4.dist-info/RECORD +12 -0
- {trismik-0.9.0.dist-info → trismik-0.9.4.dist-info}/WHEEL +1 -1
- trismik/client.py +0 -274
- trismik/runner.py +0 -85
- trismik/runner_async.py +0 -87
- trismik-0.9.0.dist-info/METADATA +0 -60
- trismik-0.9.0.dist-info/RECORD +0 -13
trismik/exceptions.py
CHANGED
|
@@ -1,13 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception classes for the Trismik client.
|
|
3
|
+
|
|
4
|
+
This module defines custom exceptions used throughout the Trismik client
|
|
5
|
+
library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
1
9
|
class TrismikError(Exception):
|
|
10
|
+
"""Base class for all exceptions raised by the Trismik package."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TrismikApiError(TrismikError):
|
|
2
14
|
"""
|
|
3
|
-
|
|
4
|
-
|
|
15
|
+
Exception raised when an error occurs during API interaction.
|
|
16
|
+
|
|
17
|
+
This exception is raised when there is an error during API communication.
|
|
5
18
|
"""
|
|
6
|
-
pass
|
|
7
19
|
|
|
8
20
|
|
|
9
|
-
class TrismikApiError
|
|
21
|
+
class TrismikPayloadTooLargeError(TrismikApiError):
|
|
22
|
+
"""
|
|
23
|
+
Exception raised when the request payload exceeds the server's size limit.
|
|
24
|
+
|
|
25
|
+
This exception is raised when a 413 "Content Too Large" error is received
|
|
26
|
+
from the API, indicating that the request payload (typically metadata)
|
|
27
|
+
exceeds the server's size limit.
|
|
10
28
|
"""
|
|
11
|
-
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str):
|
|
31
|
+
"""
|
|
32
|
+
Initialize the TrismikPayloadTooLargeError.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
message (str): The error message from the server.
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
"""Return a human-readable string representation of the exception."""
|
|
41
|
+
return f"Payload too large: {self.args[0]}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TrismikValidationError(TrismikApiError):
|
|
12
45
|
"""
|
|
13
|
-
|
|
46
|
+
Exception raised when the request fails validation.
|
|
47
|
+
|
|
48
|
+
This exception is raised when a 422 "Unprocessable Entity" error is received
|
|
49
|
+
from the API, indicating that the request failed validation (e.g., duplicate
|
|
50
|
+
item IDs, unknown item IDs in replay requests).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, message: str):
|
|
54
|
+
"""
|
|
55
|
+
Initialize the TrismikValidationError.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
message (str): The error message from the server.
|
|
59
|
+
"""
|
|
60
|
+
super().__init__(message)
|
|
61
|
+
|
|
62
|
+
def __str__(self) -> str:
|
|
63
|
+
"""Return a human-readable string representation of the exception."""
|
|
64
|
+
return f"Validation error: {self.args[0]}"
|
trismik/settings.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Default settings for the Trismik client."""
|
|
2
|
+
|
|
3
|
+
evaluation_settings = {
|
|
4
|
+
"max_iterations": 150,
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
client_settings = {"endpoint": "https://dashboard.trismik.com/api"}
|
|
8
|
+
|
|
9
|
+
# Environment variable names used by the Trismik client
|
|
10
|
+
environment_settings = {
|
|
11
|
+
# URL of the Trismik service
|
|
12
|
+
"trismik_service_url": "TRISMIK_SERVICE_URL",
|
|
13
|
+
# API key for authentication
|
|
14
|
+
"trismik_api_key": "TRISMIK_API_KEY",
|
|
15
|
+
}
|
trismik/types.py
CHANGED
|
@@ -1,133 +1,301 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@dataclass
|
|
59
|
-
class
|
|
60
|
-
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
1
|
+
"""
|
|
2
|
+
Type definitions for the Trismik client.
|
|
3
|
+
|
|
4
|
+
This module defines the data structures used throughout the Trismik client
|
|
5
|
+
library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class TrismikDataset:
|
|
15
|
+
"""Test metadata including ID and name."""
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
name: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class TrismikRun:
|
|
23
|
+
"""Run metadata including ID, URL, and status."""
|
|
24
|
+
|
|
25
|
+
id: str
|
|
26
|
+
url: str
|
|
27
|
+
status: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TrismikRunInfo:
|
|
32
|
+
"""Run info from new API endpoints."""
|
|
33
|
+
|
|
34
|
+
id: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TrismikRunState:
|
|
39
|
+
"""Run state including responses, thetas, and other metrics."""
|
|
40
|
+
|
|
41
|
+
responses: List[str]
|
|
42
|
+
thetas: List[float]
|
|
43
|
+
std_error_history: List[float]
|
|
44
|
+
kl_info_history: List[float]
|
|
45
|
+
effective_difficulties: List[float]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class TrismikRunResponse:
|
|
50
|
+
"""Response from run endpoints (start and continue)."""
|
|
51
|
+
|
|
52
|
+
run_info: TrismikRunInfo
|
|
53
|
+
state: TrismikRunState
|
|
54
|
+
next_item: Optional["TrismikItem"]
|
|
55
|
+
completed: bool
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class TrismikAdaptiveTestState:
|
|
60
|
+
"""State tracking for adaptive tests."""
|
|
61
|
+
|
|
62
|
+
run_id: str
|
|
63
|
+
state: TrismikRunState
|
|
64
|
+
completed: bool
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class AdaptiveTestScore:
|
|
69
|
+
"""Final scores of an adaptive test run."""
|
|
70
|
+
|
|
71
|
+
theta: float
|
|
72
|
+
std_error: float
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class TrismikItem:
|
|
77
|
+
"""Base class for test items."""
|
|
78
|
+
|
|
79
|
+
id: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class TrismikChoice:
|
|
84
|
+
"""Base class for choices in items that use them."""
|
|
85
|
+
|
|
86
|
+
id: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class TrismikTextChoice(TrismikChoice):
|
|
91
|
+
"""Text choice for multiple choice questions."""
|
|
92
|
+
|
|
93
|
+
text: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class TrismikMultipleChoiceTextItem(TrismikItem):
|
|
98
|
+
"""Multiple choice text question."""
|
|
99
|
+
|
|
100
|
+
question: str
|
|
101
|
+
choices: List[TrismikTextChoice]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class TrismikResult:
|
|
106
|
+
"""Test result for a specific trait."""
|
|
107
|
+
|
|
108
|
+
trait: str
|
|
109
|
+
name: str
|
|
110
|
+
value: Any
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class TrismikResponse:
|
|
115
|
+
"""Response to a test item."""
|
|
116
|
+
|
|
117
|
+
dataset_item_id: str
|
|
118
|
+
value: Any
|
|
119
|
+
correct: bool
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class TrismikRunResults:
|
|
124
|
+
"""Test results and responses."""
|
|
125
|
+
|
|
126
|
+
run_id: str
|
|
127
|
+
score: Optional[AdaptiveTestScore] = None
|
|
128
|
+
responses: Optional[List[TrismikResponse]] = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class TrismikRunSummary:
|
|
133
|
+
"""Complete run summary."""
|
|
134
|
+
|
|
135
|
+
id: str
|
|
136
|
+
dataset_id: str
|
|
137
|
+
state: TrismikRunState
|
|
138
|
+
dataset: List[TrismikItem]
|
|
139
|
+
responses: List[TrismikResponse]
|
|
140
|
+
metadata: dict
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def theta(self) -> float:
|
|
144
|
+
"""Get the theta of the run."""
|
|
145
|
+
return self.state.thetas[-1]
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def std_error(self) -> float:
|
|
149
|
+
"""Get the standard error of the run."""
|
|
150
|
+
return self.state.std_error_history[-1]
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def total_responses(self) -> int:
|
|
154
|
+
"""Get the total number of responses in the run."""
|
|
155
|
+
return len(self.responses)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def correct_responses(self) -> int:
|
|
159
|
+
"""Get the number of correct responses in the run."""
|
|
160
|
+
return sum(response.correct for response in self.responses)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def wrong_responses(self) -> int:
|
|
164
|
+
"""Get the number of wrong responses in the run."""
|
|
165
|
+
return self.total_responses - self.correct_responses
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class TrismikRunMetadata:
|
|
170
|
+
"""Metadata for a test run."""
|
|
171
|
+
|
|
172
|
+
class ModelMetadata:
|
|
173
|
+
"""Model metadata for a test run."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, name: str, **kwargs: Any):
|
|
176
|
+
"""Initialize ModelMetadata with a name and optional attributes."""
|
|
177
|
+
self.name = name
|
|
178
|
+
for key, value in kwargs.items():
|
|
179
|
+
setattr(self, key, value)
|
|
180
|
+
|
|
181
|
+
model_metadata: ModelMetadata
|
|
182
|
+
test_configuration: Dict[str, Any]
|
|
183
|
+
inference_setup: Dict[str, Any]
|
|
184
|
+
|
|
185
|
+
def toDict(self) -> Dict[str, Any]:
|
|
186
|
+
"""Convert run metadata to a dictionary."""
|
|
187
|
+
return {
|
|
188
|
+
"model_metadata": vars(self.model_metadata),
|
|
189
|
+
"test_configuration": self.test_configuration,
|
|
190
|
+
"inference_setup": self.inference_setup,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass
|
|
195
|
+
class TrismikReplayRequestItem:
|
|
196
|
+
"""Item in a replay request."""
|
|
197
|
+
|
|
198
|
+
itemId: str
|
|
199
|
+
itemChoiceId: str
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class TrismikReplayRequest:
|
|
204
|
+
"""Request to replay a run with specific responses."""
|
|
205
|
+
|
|
206
|
+
responses: List[TrismikReplayRequestItem]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass
|
|
210
|
+
class TrismikReplayResponse:
|
|
211
|
+
"""Response from a replay run request."""
|
|
212
|
+
|
|
213
|
+
id: str
|
|
214
|
+
datasetId: str
|
|
215
|
+
state: TrismikRunState
|
|
216
|
+
replay_of_run: str
|
|
217
|
+
completedAt: Optional[datetime] = None
|
|
218
|
+
createdAt: Optional[datetime] = None
|
|
219
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
220
|
+
dataset: List[TrismikItem] = field(default_factory=list)
|
|
221
|
+
responses: List[TrismikResponse] = field(default_factory=list)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass
|
|
225
|
+
class TrismikOrganization:
|
|
226
|
+
"""Organization information."""
|
|
227
|
+
|
|
228
|
+
id: str
|
|
229
|
+
name: str
|
|
230
|
+
type: str
|
|
231
|
+
role: str
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class TrismikUserInfo:
|
|
236
|
+
"""User information."""
|
|
237
|
+
|
|
238
|
+
id: str
|
|
239
|
+
email: str
|
|
240
|
+
firstname: str
|
|
241
|
+
lastname: str
|
|
242
|
+
createdAt: Optional[str] = None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclass
|
|
246
|
+
class TrismikMeResponse:
|
|
247
|
+
"""Response from the /admin/api-keys/me endpoint."""
|
|
248
|
+
|
|
249
|
+
user: TrismikUserInfo
|
|
250
|
+
organization: TrismikOrganization
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class TrismikClassicEvalItem:
|
|
255
|
+
"""Item in a classic evaluation request."""
|
|
256
|
+
|
|
257
|
+
datasetItemId: str
|
|
258
|
+
modelInput: str
|
|
259
|
+
modelOutput: str
|
|
260
|
+
goldOutput: str
|
|
261
|
+
metrics: Dict[str, Any]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass
|
|
265
|
+
class TrismikClassicEvalMetric:
|
|
266
|
+
"""Metric in a classic evaluation request."""
|
|
267
|
+
|
|
268
|
+
metricId: str
|
|
269
|
+
value: Union[str, float, int, bool]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass
|
|
273
|
+
class TrismikClassicEvalRequest:
|
|
274
|
+
"""Request to submit a classic evaluation."""
|
|
275
|
+
|
|
276
|
+
projectId: str
|
|
277
|
+
experimentName: str
|
|
278
|
+
datasetId: str
|
|
279
|
+
modelName: str
|
|
280
|
+
hyperparameters: Dict[str, Any]
|
|
281
|
+
items: List[TrismikClassicEvalItem]
|
|
282
|
+
metrics: List[TrismikClassicEvalMetric]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass
|
|
286
|
+
class TrismikClassicEvalResponse:
|
|
287
|
+
"""Response from a classic evaluation submission."""
|
|
288
|
+
|
|
289
|
+
id: str
|
|
290
|
+
organizationId: str
|
|
291
|
+
projectId: str
|
|
292
|
+
experimentId: str
|
|
293
|
+
experimentName: str
|
|
294
|
+
datasetId: str
|
|
295
|
+
userId: str
|
|
296
|
+
type: str
|
|
297
|
+
modelName: str
|
|
298
|
+
hyperparameters: Dict[str, Any]
|
|
299
|
+
createdAt: str
|
|
300
|
+
user: TrismikUserInfo
|
|
301
|
+
responseCount: int
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 Cambridge Enterprise
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Cambridge Enterprise
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|