raveforge 0.4.0__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.
- raveforge-0.4.0/PKG-INFO +297 -0
- raveforge-0.4.0/README.md +267 -0
- raveforge-0.4.0/pyproject.toml +66 -0
- raveforge-0.4.0/setup.cfg +4 -0
- raveforge-0.4.0/src/raveforge/__init__.py +31 -0
- raveforge-0.4.0/src/raveforge/core.py +346 -0
- raveforge-0.4.0/src/raveforge/diagnostics.py +470 -0
- raveforge-0.4.0/src/raveforge/enums.py +24 -0
- raveforge-0.4.0/src/raveforge/exceptions.py +49 -0
- raveforge-0.4.0/src/raveforge/rws_client.py +146 -0
- raveforge-0.4.0/src/raveforge/validator.py +269 -0
- raveforge-0.4.0/src/raveforge.egg-info/PKG-INFO +297 -0
- raveforge-0.4.0/src/raveforge.egg-info/SOURCES.txt +18 -0
- raveforge-0.4.0/src/raveforge.egg-info/dependency_links.txt +1 -0
- raveforge-0.4.0/src/raveforge.egg-info/requires.txt +6 -0
- raveforge-0.4.0/src/raveforge.egg-info/top_level.txt +1 -0
- raveforge-0.4.0/tests/test_core.py +376 -0
- raveforge-0.4.0/tests/test_diagnostics.py +351 -0
- raveforge-0.4.0/tests/test_rws_client.py +395 -0
- raveforge-0.4.0/tests/test_validator.py +318 -0
raveforge-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: raveforge
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: An elegant Python builder for Medidata Rave ODM XML.
|
|
5
|
+
Author: Mohamed Nouira
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/mnouira02/raveforge
|
|
8
|
+
Project-URL: Issues, https://github.com/mnouira02/raveforge/issues
|
|
9
|
+
Keywords: medidata,rave,odm,cdisc,clinical-trials,rws,xml
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: requests>=2.28
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# RaveForge
|
|
32
|
+
|
|
33
|
+
An elegant Python builder for Medidata Rave ODM XML.
|
|
34
|
+
|
|
35
|
+
Pushing clinical data, lab results, operational signals, or AI-derived outputs into Medidata Rave Web Services (RWS) often becomes a mess of fragile XML string concatenation, namespace issues, and hard-to-maintain hierarchy handling. RaveForge was built to make that process clean, explicit, and reliable.
|
|
36
|
+
|
|
37
|
+
RaveForge is a fluent, stateful Python engine for building RWS-friendly ODM XML without manually writing XML tags. It helps you construct properly nested `Subject > Event > Form > ItemGroup > Item` structures, optionally validates the transaction before serialisation, and optionally submits through a lightweight RWS client.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- Fluent builder API for CDISC ODM clinical data transactions
|
|
44
|
+
- Stateful hierarchy handling for `Subject > Event > Form > ItemGroup > Item`
|
|
45
|
+
- Early hierarchy validation with clear Python exceptions
|
|
46
|
+
- **Pre-build structural validation** with aggregated, location-aware error reporting
|
|
47
|
+
- Support for Medidata-specific `mdsol` extensions
|
|
48
|
+
- Query generation with configurable status and recipient
|
|
49
|
+
- Support for item `SpecifyValue`
|
|
50
|
+
- Pretty-printed XML output for debugging
|
|
51
|
+
- Thin RWS client for submission to Medidata RWS
|
|
52
|
+
- Read-only diagnostics layer for interpreting RWS submission failures
|
|
53
|
+
- Tested with `pytest`
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install raveforge
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For development:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install -e .[dev]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from raveforge import RaveTransaction, ActionType
|
|
75
|
+
|
|
76
|
+
tx = (
|
|
77
|
+
RaveTransaction(study_oid="Oncology_Phase_II_Prod")
|
|
78
|
+
.subject("SUBJ-001", site_oid="SITE-101", action=ActionType.UPSERT)
|
|
79
|
+
.event("SCREENING", repeat_key="1")
|
|
80
|
+
.form("VS", repeat_key="1")
|
|
81
|
+
.item_group("VS_GROUP", repeat_key="1")
|
|
82
|
+
.item("VSTEST", "Weight")
|
|
83
|
+
.item("VSORRES", "75")
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
xml_payload = tx.build()
|
|
87
|
+
print(tx.build_pretty())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Pre-build Validation
|
|
93
|
+
|
|
94
|
+
Call `validate()` before `build()` to catch structural and semantic problems early, with a single aggregated report instead of one exception at a time.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from raveforge import RaveTransaction, validate, ValidationError
|
|
98
|
+
|
|
99
|
+
tx = (
|
|
100
|
+
RaveTransaction(study_oid="Oncology_Phase_II_Prod")
|
|
101
|
+
.subject("SUBJ-001", site_oid="SITE-101")
|
|
102
|
+
.event("SCREENING")
|
|
103
|
+
.form("VS")
|
|
104
|
+
.item_group("VS_GROUP")
|
|
105
|
+
.item("VSORRES", "75")
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
validate(tx) # raises ValidationError if anything is wrong
|
|
110
|
+
xml_payload = tx.build()
|
|
111
|
+
except ValidationError as err:
|
|
112
|
+
print(err)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Sample output for an empty study OID and a subject with no events:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
2 validation issue(s) found:
|
|
119
|
+
[ERROR] STUDY_OID_EMPTY []: study_oid must not be empty.
|
|
120
|
+
[WARNING] SUBJECT_NO_EVENTS [SUBJ-001]: Subject 'SUBJ-001' has no events. ...
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Strict vs. non-strict mode
|
|
124
|
+
|
|
125
|
+
By default `validate()` runs in **strict mode**: both `ERROR` and `WARNING`
|
|
126
|
+
severity issues raise `ValidationError`. Pass `strict=False` to raise only on
|
|
127
|
+
errors and receive warnings as return values:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from raveforge import validate, Severity
|
|
131
|
+
|
|
132
|
+
issues = validate(tx, strict=False) # never raises on WARNING
|
|
133
|
+
warnings = [i for i in issues if i.severity == Severity.WARNING]
|
|
134
|
+
for w in warnings:
|
|
135
|
+
print(w)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### What the validator checks
|
|
139
|
+
|
|
140
|
+
| Code | Severity | Trigger |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `STUDY_OID_EMPTY` | ERROR | `study_oid` is blank or whitespace-only |
|
|
143
|
+
| `STUDY_OID_INVALID_CHARS` | ERROR | `study_oid` contains `<`, `>`, or `&` |
|
|
144
|
+
| `NO_SUBJECTS` | WARNING | Transaction has no subjects at all |
|
|
145
|
+
| `SUBJECT_NO_EVENTS` | WARNING | A subject has no events |
|
|
146
|
+
| `SITE_OID_EMPTY` | ERROR | A subject's `SiteOID` is blank |
|
|
147
|
+
| `EVENT_NO_FORMS` | WARNING | An event has no forms |
|
|
148
|
+
| `EVENT_OID_EMPTY` | ERROR | An event has a blank OID |
|
|
149
|
+
| `FORM_NO_ITEM_GROUPS` | WARNING | A form has no item groups |
|
|
150
|
+
| `FORM_OID_EMPTY` | ERROR | A form has a blank OID |
|
|
151
|
+
| `ITEM_GROUP_OID_EMPTY` | ERROR | An item group has a blank OID |
|
|
152
|
+
| `ITEM_NO_VALUE` | WARNING | An item has no value, specify, or query |
|
|
153
|
+
| `ITEM_OID_EMPTY` | ERROR | An item has a blank OID |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Queries and Specify Values
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from raveforge import (
|
|
161
|
+
RaveTransaction,
|
|
162
|
+
QueryStatus,
|
|
163
|
+
QueryRecipient,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
tx = (
|
|
167
|
+
RaveTransaction("Mediflex_Study")
|
|
168
|
+
.subject("SUBJ-1001", "SITE-NL-01")
|
|
169
|
+
.event("VISIT_1")
|
|
170
|
+
.form("LABS")
|
|
171
|
+
.item_group("LABS_IG", specified_items_only=True)
|
|
172
|
+
.item(
|
|
173
|
+
"LBTEST",
|
|
174
|
+
value="OTHER",
|
|
175
|
+
specify="Custom Biomarker Panel",
|
|
176
|
+
query="Please confirm the local lab method.",
|
|
177
|
+
query_status=QueryStatus.OPEN,
|
|
178
|
+
query_recipient=QueryRecipient.SITE_FROM_DM,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
xml_payload = tx.build()
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Submitting to RWS
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from raveforge import RaveTransaction
|
|
191
|
+
from raveforge.rws_client import RWSClient
|
|
192
|
+
|
|
193
|
+
tx = (
|
|
194
|
+
RaveTransaction("Mediflex_Study")
|
|
195
|
+
.subject("SUBJ-001", "SITE-001")
|
|
196
|
+
.event("SCREENING")
|
|
197
|
+
.form("DM")
|
|
198
|
+
.item_group("DM_IG")
|
|
199
|
+
.item("AGE", "42")
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
client = RWSClient(
|
|
203
|
+
base_url="https://innovate.mdsol.com",
|
|
204
|
+
username="username",
|
|
205
|
+
password="password",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
response = client.post_odm(tx.build())
|
|
209
|
+
print(response)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Diagnosing Submission Failures
|
|
215
|
+
|
|
216
|
+
`RaveDiagnostics` interprets RWS errors after a failed submission, performs
|
|
217
|
+
read-only lookups against RWS, and returns structured, evidence-based
|
|
218
|
+
reports. It never modifies your transaction or selects a study on your behalf.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from raveforge import RaveTransaction, RWSError, RaveDiagnostics
|
|
222
|
+
from raveforge.rws_client import RWSClient
|
|
223
|
+
|
|
224
|
+
tx = (
|
|
225
|
+
RaveTransaction("Mediflex_Dev")
|
|
226
|
+
.subject("SUBJ-001", "SITE-001")
|
|
227
|
+
.event("SCREENING")
|
|
228
|
+
.form("DM")
|
|
229
|
+
.item_group("DM_IG")
|
|
230
|
+
.item("AGE", "42")
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
client = RWSClient(
|
|
234
|
+
base_url="https://innovate.mdsol.com",
|
|
235
|
+
username="username",
|
|
236
|
+
password="password",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
diagnostics = RaveDiagnostics(client)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
client.post_odm(tx.build())
|
|
243
|
+
except RWSError as error:
|
|
244
|
+
report = diagnostics.explain_submission_failure(error, transaction=tx)
|
|
245
|
+
print(report)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Sample output for a study OID mismatch:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
[ERROR] Study Not Found
|
|
252
|
+
|
|
253
|
+
Requested:
|
|
254
|
+
study_oid: Mediflex_Dev
|
|
255
|
+
|
|
256
|
+
Evidence:
|
|
257
|
+
accessible_study_count: 4
|
|
258
|
+
suggested_matches: [{'value': 'Mediflex_DEV', 'similarity': 0.95}]
|
|
259
|
+
|
|
260
|
+
Recommendation:
|
|
261
|
+
Confirm the intended environment and use the exact StudyOID
|
|
262
|
+
configured in Rave. No payload was changed automatically.
|
|
263
|
+
|
|
264
|
+
Safe to retry automatically: False
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Why use RaveForge?
|
|
270
|
+
|
|
271
|
+
- It keeps hierarchy handling readable and safe.
|
|
272
|
+
- It validates your payload locally before bad ODM hits the network.
|
|
273
|
+
- It fails fast with precise, location-aware error messages.
|
|
274
|
+
- It reduces XML-handling boilerplate in clinical integrations.
|
|
275
|
+
- It works well in automation pipelines that generate transactional ODM.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Testing
|
|
280
|
+
|
|
281
|
+
Run the test suite with:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
pytest -v
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Contributing
|
|
290
|
+
|
|
291
|
+
Pull requests are welcome. If you add new builder or RWS features, please include or update pytest coverage.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
MIT License
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# RaveForge
|
|
2
|
+
|
|
3
|
+
An elegant Python builder for Medidata Rave ODM XML.
|
|
4
|
+
|
|
5
|
+
Pushing clinical data, lab results, operational signals, or AI-derived outputs into Medidata Rave Web Services (RWS) often becomes a mess of fragile XML string concatenation, namespace issues, and hard-to-maintain hierarchy handling. RaveForge was built to make that process clean, explicit, and reliable.
|
|
6
|
+
|
|
7
|
+
RaveForge is a fluent, stateful Python engine for building RWS-friendly ODM XML without manually writing XML tags. It helps you construct properly nested `Subject > Event > Form > ItemGroup > Item` structures, optionally validates the transaction before serialisation, and optionally submits through a lightweight RWS client.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Fluent builder API for CDISC ODM clinical data transactions
|
|
14
|
+
- Stateful hierarchy handling for `Subject > Event > Form > ItemGroup > Item`
|
|
15
|
+
- Early hierarchy validation with clear Python exceptions
|
|
16
|
+
- **Pre-build structural validation** with aggregated, location-aware error reporting
|
|
17
|
+
- Support for Medidata-specific `mdsol` extensions
|
|
18
|
+
- Query generation with configurable status and recipient
|
|
19
|
+
- Support for item `SpecifyValue`
|
|
20
|
+
- Pretty-printed XML output for debugging
|
|
21
|
+
- Thin RWS client for submission to Medidata RWS
|
|
22
|
+
- Read-only diagnostics layer for interpreting RWS submission failures
|
|
23
|
+
- Tested with `pytest`
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install raveforge
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For development:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install -e .[dev]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from raveforge import RaveTransaction, ActionType
|
|
45
|
+
|
|
46
|
+
tx = (
|
|
47
|
+
RaveTransaction(study_oid="Oncology_Phase_II_Prod")
|
|
48
|
+
.subject("SUBJ-001", site_oid="SITE-101", action=ActionType.UPSERT)
|
|
49
|
+
.event("SCREENING", repeat_key="1")
|
|
50
|
+
.form("VS", repeat_key="1")
|
|
51
|
+
.item_group("VS_GROUP", repeat_key="1")
|
|
52
|
+
.item("VSTEST", "Weight")
|
|
53
|
+
.item("VSORRES", "75")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
xml_payload = tx.build()
|
|
57
|
+
print(tx.build_pretty())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Pre-build Validation
|
|
63
|
+
|
|
64
|
+
Call `validate()` before `build()` to catch structural and semantic problems early, with a single aggregated report instead of one exception at a time.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from raveforge import RaveTransaction, validate, ValidationError
|
|
68
|
+
|
|
69
|
+
tx = (
|
|
70
|
+
RaveTransaction(study_oid="Oncology_Phase_II_Prod")
|
|
71
|
+
.subject("SUBJ-001", site_oid="SITE-101")
|
|
72
|
+
.event("SCREENING")
|
|
73
|
+
.form("VS")
|
|
74
|
+
.item_group("VS_GROUP")
|
|
75
|
+
.item("VSORRES", "75")
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
validate(tx) # raises ValidationError if anything is wrong
|
|
80
|
+
xml_payload = tx.build()
|
|
81
|
+
except ValidationError as err:
|
|
82
|
+
print(err)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Sample output for an empty study OID and a subject with no events:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
2 validation issue(s) found:
|
|
89
|
+
[ERROR] STUDY_OID_EMPTY []: study_oid must not be empty.
|
|
90
|
+
[WARNING] SUBJECT_NO_EVENTS [SUBJ-001]: Subject 'SUBJ-001' has no events. ...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Strict vs. non-strict mode
|
|
94
|
+
|
|
95
|
+
By default `validate()` runs in **strict mode**: both `ERROR` and `WARNING`
|
|
96
|
+
severity issues raise `ValidationError`. Pass `strict=False` to raise only on
|
|
97
|
+
errors and receive warnings as return values:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from raveforge import validate, Severity
|
|
101
|
+
|
|
102
|
+
issues = validate(tx, strict=False) # never raises on WARNING
|
|
103
|
+
warnings = [i for i in issues if i.severity == Severity.WARNING]
|
|
104
|
+
for w in warnings:
|
|
105
|
+
print(w)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### What the validator checks
|
|
109
|
+
|
|
110
|
+
| Code | Severity | Trigger |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `STUDY_OID_EMPTY` | ERROR | `study_oid` is blank or whitespace-only |
|
|
113
|
+
| `STUDY_OID_INVALID_CHARS` | ERROR | `study_oid` contains `<`, `>`, or `&` |
|
|
114
|
+
| `NO_SUBJECTS` | WARNING | Transaction has no subjects at all |
|
|
115
|
+
| `SUBJECT_NO_EVENTS` | WARNING | A subject has no events |
|
|
116
|
+
| `SITE_OID_EMPTY` | ERROR | A subject's `SiteOID` is blank |
|
|
117
|
+
| `EVENT_NO_FORMS` | WARNING | An event has no forms |
|
|
118
|
+
| `EVENT_OID_EMPTY` | ERROR | An event has a blank OID |
|
|
119
|
+
| `FORM_NO_ITEM_GROUPS` | WARNING | A form has no item groups |
|
|
120
|
+
| `FORM_OID_EMPTY` | ERROR | A form has a blank OID |
|
|
121
|
+
| `ITEM_GROUP_OID_EMPTY` | ERROR | An item group has a blank OID |
|
|
122
|
+
| `ITEM_NO_VALUE` | WARNING | An item has no value, specify, or query |
|
|
123
|
+
| `ITEM_OID_EMPTY` | ERROR | An item has a blank OID |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Queries and Specify Values
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from raveforge import (
|
|
131
|
+
RaveTransaction,
|
|
132
|
+
QueryStatus,
|
|
133
|
+
QueryRecipient,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
tx = (
|
|
137
|
+
RaveTransaction("Mediflex_Study")
|
|
138
|
+
.subject("SUBJ-1001", "SITE-NL-01")
|
|
139
|
+
.event("VISIT_1")
|
|
140
|
+
.form("LABS")
|
|
141
|
+
.item_group("LABS_IG", specified_items_only=True)
|
|
142
|
+
.item(
|
|
143
|
+
"LBTEST",
|
|
144
|
+
value="OTHER",
|
|
145
|
+
specify="Custom Biomarker Panel",
|
|
146
|
+
query="Please confirm the local lab method.",
|
|
147
|
+
query_status=QueryStatus.OPEN,
|
|
148
|
+
query_recipient=QueryRecipient.SITE_FROM_DM,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
xml_payload = tx.build()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Submitting to RWS
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from raveforge import RaveTransaction
|
|
161
|
+
from raveforge.rws_client import RWSClient
|
|
162
|
+
|
|
163
|
+
tx = (
|
|
164
|
+
RaveTransaction("Mediflex_Study")
|
|
165
|
+
.subject("SUBJ-001", "SITE-001")
|
|
166
|
+
.event("SCREENING")
|
|
167
|
+
.form("DM")
|
|
168
|
+
.item_group("DM_IG")
|
|
169
|
+
.item("AGE", "42")
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
client = RWSClient(
|
|
173
|
+
base_url="https://innovate.mdsol.com",
|
|
174
|
+
username="username",
|
|
175
|
+
password="password",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
response = client.post_odm(tx.build())
|
|
179
|
+
print(response)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Diagnosing Submission Failures
|
|
185
|
+
|
|
186
|
+
`RaveDiagnostics` interprets RWS errors after a failed submission, performs
|
|
187
|
+
read-only lookups against RWS, and returns structured, evidence-based
|
|
188
|
+
reports. It never modifies your transaction or selects a study on your behalf.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from raveforge import RaveTransaction, RWSError, RaveDiagnostics
|
|
192
|
+
from raveforge.rws_client import RWSClient
|
|
193
|
+
|
|
194
|
+
tx = (
|
|
195
|
+
RaveTransaction("Mediflex_Dev")
|
|
196
|
+
.subject("SUBJ-001", "SITE-001")
|
|
197
|
+
.event("SCREENING")
|
|
198
|
+
.form("DM")
|
|
199
|
+
.item_group("DM_IG")
|
|
200
|
+
.item("AGE", "42")
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
client = RWSClient(
|
|
204
|
+
base_url="https://innovate.mdsol.com",
|
|
205
|
+
username="username",
|
|
206
|
+
password="password",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
diagnostics = RaveDiagnostics(client)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
client.post_odm(tx.build())
|
|
213
|
+
except RWSError as error:
|
|
214
|
+
report = diagnostics.explain_submission_failure(error, transaction=tx)
|
|
215
|
+
print(report)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Sample output for a study OID mismatch:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
[ERROR] Study Not Found
|
|
222
|
+
|
|
223
|
+
Requested:
|
|
224
|
+
study_oid: Mediflex_Dev
|
|
225
|
+
|
|
226
|
+
Evidence:
|
|
227
|
+
accessible_study_count: 4
|
|
228
|
+
suggested_matches: [{'value': 'Mediflex_DEV', 'similarity': 0.95}]
|
|
229
|
+
|
|
230
|
+
Recommendation:
|
|
231
|
+
Confirm the intended environment and use the exact StudyOID
|
|
232
|
+
configured in Rave. No payload was changed automatically.
|
|
233
|
+
|
|
234
|
+
Safe to retry automatically: False
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Why use RaveForge?
|
|
240
|
+
|
|
241
|
+
- It keeps hierarchy handling readable and safe.
|
|
242
|
+
- It validates your payload locally before bad ODM hits the network.
|
|
243
|
+
- It fails fast with precise, location-aware error messages.
|
|
244
|
+
- It reduces XML-handling boilerplate in clinical integrations.
|
|
245
|
+
- It works well in automation pipelines that generate transactional ODM.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Testing
|
|
250
|
+
|
|
251
|
+
Run the test suite with:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pytest -v
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Contributing
|
|
260
|
+
|
|
261
|
+
Pull requests are welcome. If you add new builder or RWS features, please include or update pytest coverage.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
MIT License
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "raveforge"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "An elegant Python builder for Medidata Rave ODM XML."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Mohamed Nouira" }
|
|
13
|
+
]
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
keywords = [
|
|
16
|
+
"medidata",
|
|
17
|
+
"rave",
|
|
18
|
+
"odm",
|
|
19
|
+
"cdisc",
|
|
20
|
+
"clinical-trials",
|
|
21
|
+
"rws",
|
|
22
|
+
"xml"
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"Intended Audience :: Science/Research",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
32
|
+
"Programming Language :: Python :: 3.8",
|
|
33
|
+
"Programming Language :: Python :: 3.9",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps."
|
|
38
|
+
]
|
|
39
|
+
dependencies = [
|
|
40
|
+
"requests>=2.28"
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest>=7.0",
|
|
46
|
+
"pytest-cov>=4.0",
|
|
47
|
+
"ruff>=0.4"
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Repository = "https://github.com/mnouira02/raveforge"
|
|
52
|
+
Issues = "https://github.com/mnouira02/raveforge/issues"
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
pythonpath = ["src"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 100
|
|
60
|
+
target-version = "py38"
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["E", "F", "W", "I"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.isort]
|
|
66
|
+
known-first-party = ["raveforge"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .core import RaveTransaction
|
|
2
|
+
from .diagnostics import DiagnosticReport, RaveDiagnostics
|
|
3
|
+
from .enums import ActionType, QueryRecipient, QueryStatus
|
|
4
|
+
from .exceptions import HierarchyError, RaveForgeError, RWSError, ValidationError
|
|
5
|
+
from .rws_client import RWSClient
|
|
6
|
+
from .validator import Severity, ValidationIssue, validate
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
# Core builder
|
|
10
|
+
"RaveTransaction",
|
|
11
|
+
# Enums
|
|
12
|
+
"ActionType",
|
|
13
|
+
"QueryStatus",
|
|
14
|
+
"QueryRecipient",
|
|
15
|
+
# Exceptions
|
|
16
|
+
"RaveForgeError",
|
|
17
|
+
"HierarchyError",
|
|
18
|
+
"ValidationError",
|
|
19
|
+
"RWSError",
|
|
20
|
+
# HTTP client
|
|
21
|
+
"RWSClient",
|
|
22
|
+
# Diagnostics
|
|
23
|
+
"RaveDiagnostics",
|
|
24
|
+
"DiagnosticReport",
|
|
25
|
+
# Validation
|
|
26
|
+
"validate",
|
|
27
|
+
"ValidationIssue",
|
|
28
|
+
"Severity",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
__version__ = "0.4.0"
|