reflex-icd11ect 0.1.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.
- reflex_icd11ect-0.1.0/LICENSE +21 -0
- reflex_icd11ect-0.1.0/PKG-INFO +341 -0
- reflex_icd11ect-0.1.0/README.md +303 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/__init__.py +105 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/_runtime.py +291 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/constants.py +194 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/handler.py +242 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/icd11ect.py +552 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/icd11ect.pyi +512 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/namespace.py +49 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/namespace.pyi +193 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/py.typed +0 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/token.py +224 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect/types.py +164 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect.egg-info/PKG-INFO +341 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect.egg-info/SOURCES.txt +25 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect.egg-info/dependency_links.txt +1 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect.egg-info/requires.txt +14 -0
- reflex_icd11ect-0.1.0/custom_components/reflex_icd11ect.egg-info/top_level.txt +1 -0
- reflex_icd11ect-0.1.0/pyproject.toml +95 -0
- reflex_icd11ect-0.1.0/setup.cfg +4 -0
- reflex_icd11ect-0.1.0/tests/test_components.py +246 -0
- reflex_icd11ect-0.1.0/tests/test_handler.py +70 -0
- reflex_icd11ect-0.1.0/tests/test_packaging.py +70 -0
- reflex_icd11ect-0.1.0/tests/test_runtime.py +193 -0
- reflex_icd11ect-0.1.0/tests/test_token.py +180 -0
- reflex_icd11ect-0.1.0/tests/test_types.py +38 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ernesto Crespo
|
|
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.
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reflex-icd11ect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reflex custom component for the WHO ICD-11 Embedded Classification Tools (Embedded Coding Tool and Embedded Browser)
|
|
5
|
+
Author-email: Ernesto Crespo <ecrespo@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ecrespo/reflex-icd11ect
|
|
8
|
+
Project-URL: Source, https://github.com/ecrespo/reflex-icd11ect
|
|
9
|
+
Project-URL: Issues, https://github.com/ecrespo/reflex-icd11ect/issues
|
|
10
|
+
Project-URL: ECT documentation, https://icd.who.int/docs/icd-api/icd11ect/
|
|
11
|
+
Keywords: reflex,reflex-custom-components,icd11,icd-11,icd11ect,who,medical-coding,healthcare,terminology
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
21
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: reflex>=0.9.0
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build; extra == "dev"
|
|
29
|
+
Requires-Dist: twine; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
Requires-Dist: trove-classifiers; extra == "dev"
|
|
34
|
+
Provides-Extra: security
|
|
35
|
+
Requires-Dist: bandit[toml]; extra == "security"
|
|
36
|
+
Requires-Dist: pip-audit; extra == "security"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# reflex-icd11ect
|
|
40
|
+
|
|
41
|
+
[](https://github.com/ecrespo/reflex-icd11ect/actions/workflows/tests.yml)
|
|
42
|
+
[](https://github.com/ecrespo/reflex-icd11ect/actions/workflows/quality.yml)
|
|
43
|
+
[](https://github.com/ecrespo/reflex-icd11ect/actions/workflows/security.yml)
|
|
44
|
+
[](https://pypi.org/project/reflex-icd11ect/)
|
|
45
|
+
[](https://pypi.org/project/reflex-icd11ect/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
A [Reflex](https://reflex.dev) custom component for the WHO **ICD-11 Embedded
|
|
49
|
+
Classification Tools** ([ECT](https://icd.who.int/docs/icd-api/icd11ect/)): the
|
|
50
|
+
*Embedded Coding Tool* and the *Embedded Browser*, powered by the ICD-API.
|
|
51
|
+
|
|
52
|
+
It wraps [`@whoicd/icd11ect`](https://www.npmjs.com/package/@whoicd/icd11ect)
|
|
53
|
+
1.8 and turns its imperative, page-global JavaScript API into ordinary Reflex
|
|
54
|
+
components, props and event handlers.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import reflex as rx
|
|
58
|
+
from reflex_icd11ect import icd11ect
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class State(rx.State):
|
|
62
|
+
code: str = ""
|
|
63
|
+
title: str = ""
|
|
64
|
+
|
|
65
|
+
@rx.event
|
|
66
|
+
def on_select(self, entity: dict[str, str]):
|
|
67
|
+
self.code = entity["code"]
|
|
68
|
+
self.title = entity["title"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def index() -> rx.Component:
|
|
72
|
+
return rx.vstack(
|
|
73
|
+
rx.heading(f"{State.code} {State.title}"),
|
|
74
|
+
icd11ect.coding_tool(
|
|
75
|
+
api_server_url="http://localhost", # your ICD-API deployment
|
|
76
|
+
placeholder="Search the ICD-11...",
|
|
77
|
+
on_select=State.on_select,
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Install
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install reflex-icd11ect
|
|
86
|
+
# or
|
|
87
|
+
uv add reflex-icd11ect
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The npm package and its stylesheet are installed into the frontend by Reflex;
|
|
91
|
+
there is nothing to add to `rxconfig.py`.
|
|
92
|
+
|
|
93
|
+
## What you get
|
|
94
|
+
|
|
95
|
+
| Component | Renders |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `icd11ect.coding_tool(...)` | Search box + result window, configured and bound |
|
|
98
|
+
| `icd11ect.browser(...)` | The Embedded Browser |
|
|
99
|
+
| `icd11ect.controller(...)` | Lifecycle only, renders nothing |
|
|
100
|
+
| `icd11ect.provider(...)` | The controller used as page-wide settings |
|
|
101
|
+
| `icd11ect.search_input(ino)` | The bare `input.ctw-input` element |
|
|
102
|
+
| `icd11ect.result_window(ino)` | The bare `div.ctw-window` element |
|
|
103
|
+
| `icd11ect.browser_window(ino)` | The bare `div.ctw-eb-window` element |
|
|
104
|
+
|
|
105
|
+
Plus `reflex_icd11ect.handler` (the imperative `ECT.Handler` API as Reflex
|
|
106
|
+
events), `IcdTokenProvider` (OAUTH 2.0 client credentials, backend side),
|
|
107
|
+
`SelectedEntity` / `BrowserContent` (typed event payloads) and the constants
|
|
108
|
+
(`MMS_CHAPTERS`, `LANGUAGES`, `SOURCES`, ...).
|
|
109
|
+
|
|
110
|
+
The classes are available too: `Icd11ectCodingTool`, `Icd11ectBrowser`,
|
|
111
|
+
`Icd11ectController`.
|
|
112
|
+
|
|
113
|
+
## Choosing a server
|
|
114
|
+
|
|
115
|
+
`api_server_url` is the only required setting.
|
|
116
|
+
|
|
117
|
+
| Deployment | Settings |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| Local ICD-API (Docker, Windows or systemd service) | `api_server_url="http://localhost"`, `api_secured=False` |
|
|
120
|
+
| WHO cloud API | `api_server_url="https://id.who.int"`, `api_secured=True` + a token |
|
|
121
|
+
| WHO developer test server (development only) | `api_server_url=WHO_DEVELOPER_TEST_API`, `api_secured=False` |
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# a local deployment, no credentials, no data leaving your network
|
|
125
|
+
docker run -p 80:80 -e acceptLicense=true -e saveAnalytics=true whoicd/icd-api
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Events
|
|
129
|
+
|
|
130
|
+
| Prop | ECT callback | Payload |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `on_select` | `selectedEntityFunction` | `code`, `title`, `uri`, `linearization_uri`, `foundation_uri`, `selected_text`, `search_query`, `i_no` |
|
|
133
|
+
| `on_search_start` | `searchStartedFunction` | none |
|
|
134
|
+
| `on_search_end` | `searchEndedFunction` | none |
|
|
135
|
+
| `on_browser_load` | `browserLoadedFunction` | none |
|
|
136
|
+
| `on_browser_change` | `browserChangedFunction` | `code`, `uri`, `i_no` |
|
|
137
|
+
| `on_token_request` | `getNewTokenFunction` | none |
|
|
138
|
+
|
|
139
|
+
Payload keys are snake_case; wrap them for attribute access:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from reflex_icd11ect import SelectedEntity
|
|
143
|
+
|
|
144
|
+
@rx.event
|
|
145
|
+
def on_select(self, entity: dict[str, str]):
|
|
146
|
+
selected = SelectedEntity.from_payload(entity)
|
|
147
|
+
if selected.is_postcoordinated:
|
|
148
|
+
...
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`on_search_start` / `on_search_end` carry no instance id in ECT, so with several
|
|
152
|
+
instances on a page they are delivered to the instance whose search box has
|
|
153
|
+
focus, falling back to the one that last emitted an event.
|
|
154
|
+
|
|
155
|
+
## Driving the tool from the backend
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from reflex_icd11ect import handler
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class State(rx.State):
|
|
162
|
+
@rx.event
|
|
163
|
+
def code_fever(self):
|
|
164
|
+
return handler.search("1", "fever")
|
|
165
|
+
|
|
166
|
+
@rx.event
|
|
167
|
+
def show_tuberculosis(self):
|
|
168
|
+
return handler.set_browser_code("browser", "1B11")
|
|
169
|
+
|
|
170
|
+
@rx.event
|
|
171
|
+
def spanish(self):
|
|
172
|
+
return handler.change_language("1", "es")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`search`, `clear`, `set_browser_code`, `set_browser_uri`, `change_language`,
|
|
176
|
+
`change_source`, `change_minor_version`, `overwrite_configuration`, `bind` and
|
|
177
|
+
`set_token` are available. They run entirely on the client, with no extra round
|
|
178
|
+
trip.
|
|
179
|
+
|
|
180
|
+
## OAUTH 2.0 with the WHO cloud API
|
|
181
|
+
|
|
182
|
+
Register at <https://icd.who.int/icdapi> for a client id and secret, then keep
|
|
183
|
+
the secret on the backend:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from reflex_icd11ect import IcdTokenProvider, icd11ect
|
|
187
|
+
|
|
188
|
+
provider = IcdTokenProvider.from_env() # ICD_CLIENT_ID / ICD_CLIENT_SECRET
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class Auth(rx.State):
|
|
192
|
+
token: str = ""
|
|
193
|
+
|
|
194
|
+
@rx.event(background=True)
|
|
195
|
+
async def refresh_token(self):
|
|
196
|
+
token = await provider.async_token()
|
|
197
|
+
async with self:
|
|
198
|
+
self.token = token
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
icd11ect.coding_tool(
|
|
202
|
+
api_server_url="https://id.who.int",
|
|
203
|
+
api_secured=True,
|
|
204
|
+
token=Auth.token,
|
|
205
|
+
on_token_request=Auth.refresh_token, # fired when the token expires
|
|
206
|
+
on_select=State.on_select,
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`IcdTokenProvider` caches the token and refreshes it five minutes before it
|
|
211
|
+
expires. Only the short-lived token reaches the browser.
|
|
212
|
+
|
|
213
|
+
If you prefer the pattern WHO documents, where the browser fetches the token
|
|
214
|
+
itself, set `token_endpoint="/api/icd/token"` (and `token_field` when the JSON
|
|
215
|
+
field is not named `token`) instead of `token`.
|
|
216
|
+
|
|
217
|
+
## Custom layouts
|
|
218
|
+
|
|
219
|
+
`coding_tool` is a `controller` with the two elements as children. Split them
|
|
220
|
+
when the search box and the results belong in different places:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
rx.fragment(
|
|
224
|
+
icd11ect.controller(ino="dx", api_server_url=..., on_select=State.on_select),
|
|
225
|
+
rx.hstack(
|
|
226
|
+
rx.text("Diagnosis"),
|
|
227
|
+
icd11ect.search_input("dx", placeholder="..."),
|
|
228
|
+
),
|
|
229
|
+
rx.card(icd11ect.result_window("dx")),
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The search box must stay uncontrolled: ECT writes into its value and disables it
|
|
234
|
+
while it loads, so do not bind `value` to your state.
|
|
235
|
+
|
|
236
|
+
## Several instances
|
|
237
|
+
|
|
238
|
+
`ino` is ECT's `data-ctw-ino`; it identifies an instance and must be unique on
|
|
239
|
+
the page. ECT keeps a single configuration per page, so the component merges the
|
|
240
|
+
settings of every mounted instance and applies what an instance sets differently
|
|
241
|
+
through `ECT.Handler.overwriteConfiguration`:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
icd11ect.coding_tool(ino="a", language="en", chapters_filter="")
|
|
245
|
+
icd11ect.coding_tool(ino="b", language="es", chapters_filter="06")
|
|
246
|
+
icd11ect.browser(ino="c", enable_select_button="categories")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Only part of ECT's settings can differ per instance: `api_server_url`,
|
|
250
|
+
`api_secured`, `source`, `minor_version`, `language`, `popup_mode`,
|
|
251
|
+
`simplified_mode`, `disable_hierarchy`, `words_available`, `chapters_available`,
|
|
252
|
+
`chapters_filter`, `subtrees_filter`, `flexisearch_available`,
|
|
253
|
+
`search_by_code_or_uri`, `hierarchy_title` and `height`
|
|
254
|
+
(`reflex_icd11ect.OVERWRITABLE_SETTINGS`). The rest is page-wide and takes the
|
|
255
|
+
value of the last instance that set it.
|
|
256
|
+
|
|
257
|
+
## Settings
|
|
258
|
+
|
|
259
|
+
Every ECT setting is a prop, snake_case instead of camelCase. Props you do not
|
|
260
|
+
set are not sent, so ECT keeps its own defaults; an empty string is treated the
|
|
261
|
+
same way, which makes state-driven props easy.
|
|
262
|
+
|
|
263
|
+
**Shared** — `api_server_url`, `api_secured`, `source` (`"mms"`, `"icf"`,
|
|
264
|
+
`"foundation"`), `minor_version`, `language`, `source_app`, `height`,
|
|
265
|
+
`hierarchy_title`, `hierarchy_resizable`, `other_postcoordination`,
|
|
266
|
+
`enable_keyboard`, `include_diagnostic_criteria`, `verbose`.
|
|
267
|
+
|
|
268
|
+
**Coding Tool** — `popup_mode`, `simplified_mode`, `disable_hierarchy`,
|
|
269
|
+
`words_available`, `chapters_available`, `chapters_filter`, `subtrees_filter`,
|
|
270
|
+
`flexisearch_available`, `search_by_code_or_uri`, `medical_coding_mode`,
|
|
271
|
+
`view_selected_uri`.
|
|
272
|
+
|
|
273
|
+
**Embedded Browser** — `enable_select_button` (`"none"`, `"categories"`,
|
|
274
|
+
`"all"`, `"allButRoot"`), `browser_search_available`,
|
|
275
|
+
`browser_advanced_search_available`, `browser_hierarchy_available`,
|
|
276
|
+
`browser_hierarchy_root_uris`, `browser_uri`,
|
|
277
|
+
`display_other_foundation_children`.
|
|
278
|
+
|
|
279
|
+
Every component accepts every setting: ECT keeps a single configuration per
|
|
280
|
+
page, so a Coding Tool setting passed to a browser would still take effect on
|
|
281
|
+
the page's Coding Tools, and hiding it would only mislead.
|
|
282
|
+
|
|
283
|
+
Bridge-only props: `token`, `token_endpoint`, `token_field`,
|
|
284
|
+
`token_timeout_ms`, `ino`, plus `placeholder`, `input_props` and `window_props`
|
|
285
|
+
for styling the elements.
|
|
286
|
+
|
|
287
|
+
`docs/settings.md` has the full table with ECT names and per-instance support.
|
|
288
|
+
|
|
289
|
+
## How it works
|
|
290
|
+
|
|
291
|
+
ECT is not a React component: it renders itself into DOM nodes carrying
|
|
292
|
+
`data-ctw-ino`, and keeps one global configuration and one set of callbacks for
|
|
293
|
+
the whole page. The component therefore:
|
|
294
|
+
|
|
295
|
+
* renders the markup ECT looks for, with your `ino`;
|
|
296
|
+
* emits a page-level runtime (`window.__reflexIcd11ect`) that owns the ECT
|
|
297
|
+
configuration and routes ECT's global callbacks to the right instance;
|
|
298
|
+
* configures ECT with `autoBind: false` and calls `ECT.Handler.bind(ino)` from a
|
|
299
|
+
`useEffect`, because ECT's auto-binding waits for `window.onload`, an event
|
|
300
|
+
that has already fired by the time a single-page app renders a route;
|
|
301
|
+
* re-applies settings and rebinds whenever your props change.
|
|
302
|
+
|
|
303
|
+
`docs/architecture.md` goes into detail.
|
|
304
|
+
|
|
305
|
+
## Demo app
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
git clone https://github.com/ecrespo/reflex-icd11ect
|
|
309
|
+
cd reflex-icd11ect
|
|
310
|
+
uv pip install -e .
|
|
311
|
+
cd icd11ect_demo && uv run reflex run
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Six pages: the Coding Tool with every setting live, the Embedded Browser,
|
|
315
|
+
several instances side by side, custom layouts, server and OAUTH 2.0 handling,
|
|
316
|
+
and a generated API reference.
|
|
317
|
+
|
|
318
|
+
## Documentation
|
|
319
|
+
|
|
320
|
+
* [`docs/settings.md`](docs/settings.md) — every prop, its ECT name and whether
|
|
321
|
+
it can differ per instance
|
|
322
|
+
* [`docs/architecture.md`](docs/architecture.md) — how the bridge works
|
|
323
|
+
* [`docs/troubleshooting.md`](docs/troubleshooting.md) — what to check when
|
|
324
|
+
nothing renders
|
|
325
|
+
* [ECT documentation](https://icd.who.int/docs/icd-api/icd11ect/) — WHO
|
|
326
|
+
* [ICD-API documentation](https://icd.who.int/docs/icd-api/) — WHO
|
|
327
|
+
* [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to set up, test and release
|
|
328
|
+
* [`SECURITY.md`](SECURITY.md) — reporting a vulnerability, and how to keep
|
|
329
|
+
the ICD-API credentials out of the browser
|
|
330
|
+
|
|
331
|
+
## Requirements
|
|
332
|
+
|
|
333
|
+
* Python >= 3.10, Reflex >= 0.9
|
|
334
|
+
* An ICD-API server (local deployment or the WHO cloud API)
|
|
335
|
+
|
|
336
|
+
## Licence
|
|
337
|
+
|
|
338
|
+
MIT for this component. The ICD-11 and the Embedded Classification Tools are
|
|
339
|
+
published by the World Health Organization under their own licence: see
|
|
340
|
+
<https://icd.who.int/en/docs/icd11-license.pdf> and the
|
|
341
|
+
[ICD-API terms](https://icd.who.int/icdapi).
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# reflex-icd11ect
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ecrespo/reflex-icd11ect/actions/workflows/tests.yml)
|
|
4
|
+
[](https://github.com/ecrespo/reflex-icd11ect/actions/workflows/quality.yml)
|
|
5
|
+
[](https://github.com/ecrespo/reflex-icd11ect/actions/workflows/security.yml)
|
|
6
|
+
[](https://pypi.org/project/reflex-icd11ect/)
|
|
7
|
+
[](https://pypi.org/project/reflex-icd11ect/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
A [Reflex](https://reflex.dev) custom component for the WHO **ICD-11 Embedded
|
|
11
|
+
Classification Tools** ([ECT](https://icd.who.int/docs/icd-api/icd11ect/)): the
|
|
12
|
+
*Embedded Coding Tool* and the *Embedded Browser*, powered by the ICD-API.
|
|
13
|
+
|
|
14
|
+
It wraps [`@whoicd/icd11ect`](https://www.npmjs.com/package/@whoicd/icd11ect)
|
|
15
|
+
1.8 and turns its imperative, page-global JavaScript API into ordinary Reflex
|
|
16
|
+
components, props and event handlers.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import reflex as rx
|
|
20
|
+
from reflex_icd11ect import icd11ect
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class State(rx.State):
|
|
24
|
+
code: str = ""
|
|
25
|
+
title: str = ""
|
|
26
|
+
|
|
27
|
+
@rx.event
|
|
28
|
+
def on_select(self, entity: dict[str, str]):
|
|
29
|
+
self.code = entity["code"]
|
|
30
|
+
self.title = entity["title"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def index() -> rx.Component:
|
|
34
|
+
return rx.vstack(
|
|
35
|
+
rx.heading(f"{State.code} {State.title}"),
|
|
36
|
+
icd11ect.coding_tool(
|
|
37
|
+
api_server_url="http://localhost", # your ICD-API deployment
|
|
38
|
+
placeholder="Search the ICD-11...",
|
|
39
|
+
on_select=State.on_select,
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install reflex-icd11ect
|
|
48
|
+
# or
|
|
49
|
+
uv add reflex-icd11ect
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The npm package and its stylesheet are installed into the frontend by Reflex;
|
|
53
|
+
there is nothing to add to `rxconfig.py`.
|
|
54
|
+
|
|
55
|
+
## What you get
|
|
56
|
+
|
|
57
|
+
| Component | Renders |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `icd11ect.coding_tool(...)` | Search box + result window, configured and bound |
|
|
60
|
+
| `icd11ect.browser(...)` | The Embedded Browser |
|
|
61
|
+
| `icd11ect.controller(...)` | Lifecycle only, renders nothing |
|
|
62
|
+
| `icd11ect.provider(...)` | The controller used as page-wide settings |
|
|
63
|
+
| `icd11ect.search_input(ino)` | The bare `input.ctw-input` element |
|
|
64
|
+
| `icd11ect.result_window(ino)` | The bare `div.ctw-window` element |
|
|
65
|
+
| `icd11ect.browser_window(ino)` | The bare `div.ctw-eb-window` element |
|
|
66
|
+
|
|
67
|
+
Plus `reflex_icd11ect.handler` (the imperative `ECT.Handler` API as Reflex
|
|
68
|
+
events), `IcdTokenProvider` (OAUTH 2.0 client credentials, backend side),
|
|
69
|
+
`SelectedEntity` / `BrowserContent` (typed event payloads) and the constants
|
|
70
|
+
(`MMS_CHAPTERS`, `LANGUAGES`, `SOURCES`, ...).
|
|
71
|
+
|
|
72
|
+
The classes are available too: `Icd11ectCodingTool`, `Icd11ectBrowser`,
|
|
73
|
+
`Icd11ectController`.
|
|
74
|
+
|
|
75
|
+
## Choosing a server
|
|
76
|
+
|
|
77
|
+
`api_server_url` is the only required setting.
|
|
78
|
+
|
|
79
|
+
| Deployment | Settings |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| Local ICD-API (Docker, Windows or systemd service) | `api_server_url="http://localhost"`, `api_secured=False` |
|
|
82
|
+
| WHO cloud API | `api_server_url="https://id.who.int"`, `api_secured=True` + a token |
|
|
83
|
+
| WHO developer test server (development only) | `api_server_url=WHO_DEVELOPER_TEST_API`, `api_secured=False` |
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# a local deployment, no credentials, no data leaving your network
|
|
87
|
+
docker run -p 80:80 -e acceptLicense=true -e saveAnalytics=true whoicd/icd-api
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Events
|
|
91
|
+
|
|
92
|
+
| Prop | ECT callback | Payload |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `on_select` | `selectedEntityFunction` | `code`, `title`, `uri`, `linearization_uri`, `foundation_uri`, `selected_text`, `search_query`, `i_no` |
|
|
95
|
+
| `on_search_start` | `searchStartedFunction` | none |
|
|
96
|
+
| `on_search_end` | `searchEndedFunction` | none |
|
|
97
|
+
| `on_browser_load` | `browserLoadedFunction` | none |
|
|
98
|
+
| `on_browser_change` | `browserChangedFunction` | `code`, `uri`, `i_no` |
|
|
99
|
+
| `on_token_request` | `getNewTokenFunction` | none |
|
|
100
|
+
|
|
101
|
+
Payload keys are snake_case; wrap them for attribute access:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from reflex_icd11ect import SelectedEntity
|
|
105
|
+
|
|
106
|
+
@rx.event
|
|
107
|
+
def on_select(self, entity: dict[str, str]):
|
|
108
|
+
selected = SelectedEntity.from_payload(entity)
|
|
109
|
+
if selected.is_postcoordinated:
|
|
110
|
+
...
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`on_search_start` / `on_search_end` carry no instance id in ECT, so with several
|
|
114
|
+
instances on a page they are delivered to the instance whose search box has
|
|
115
|
+
focus, falling back to the one that last emitted an event.
|
|
116
|
+
|
|
117
|
+
## Driving the tool from the backend
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from reflex_icd11ect import handler
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class State(rx.State):
|
|
124
|
+
@rx.event
|
|
125
|
+
def code_fever(self):
|
|
126
|
+
return handler.search("1", "fever")
|
|
127
|
+
|
|
128
|
+
@rx.event
|
|
129
|
+
def show_tuberculosis(self):
|
|
130
|
+
return handler.set_browser_code("browser", "1B11")
|
|
131
|
+
|
|
132
|
+
@rx.event
|
|
133
|
+
def spanish(self):
|
|
134
|
+
return handler.change_language("1", "es")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`search`, `clear`, `set_browser_code`, `set_browser_uri`, `change_language`,
|
|
138
|
+
`change_source`, `change_minor_version`, `overwrite_configuration`, `bind` and
|
|
139
|
+
`set_token` are available. They run entirely on the client, with no extra round
|
|
140
|
+
trip.
|
|
141
|
+
|
|
142
|
+
## OAUTH 2.0 with the WHO cloud API
|
|
143
|
+
|
|
144
|
+
Register at <https://icd.who.int/icdapi> for a client id and secret, then keep
|
|
145
|
+
the secret on the backend:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from reflex_icd11ect import IcdTokenProvider, icd11ect
|
|
149
|
+
|
|
150
|
+
provider = IcdTokenProvider.from_env() # ICD_CLIENT_ID / ICD_CLIENT_SECRET
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Auth(rx.State):
|
|
154
|
+
token: str = ""
|
|
155
|
+
|
|
156
|
+
@rx.event(background=True)
|
|
157
|
+
async def refresh_token(self):
|
|
158
|
+
token = await provider.async_token()
|
|
159
|
+
async with self:
|
|
160
|
+
self.token = token
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
icd11ect.coding_tool(
|
|
164
|
+
api_server_url="https://id.who.int",
|
|
165
|
+
api_secured=True,
|
|
166
|
+
token=Auth.token,
|
|
167
|
+
on_token_request=Auth.refresh_token, # fired when the token expires
|
|
168
|
+
on_select=State.on_select,
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`IcdTokenProvider` caches the token and refreshes it five minutes before it
|
|
173
|
+
expires. Only the short-lived token reaches the browser.
|
|
174
|
+
|
|
175
|
+
If you prefer the pattern WHO documents, where the browser fetches the token
|
|
176
|
+
itself, set `token_endpoint="/api/icd/token"` (and `token_field` when the JSON
|
|
177
|
+
field is not named `token`) instead of `token`.
|
|
178
|
+
|
|
179
|
+
## Custom layouts
|
|
180
|
+
|
|
181
|
+
`coding_tool` is a `controller` with the two elements as children. Split them
|
|
182
|
+
when the search box and the results belong in different places:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
rx.fragment(
|
|
186
|
+
icd11ect.controller(ino="dx", api_server_url=..., on_select=State.on_select),
|
|
187
|
+
rx.hstack(
|
|
188
|
+
rx.text("Diagnosis"),
|
|
189
|
+
icd11ect.search_input("dx", placeholder="..."),
|
|
190
|
+
),
|
|
191
|
+
rx.card(icd11ect.result_window("dx")),
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The search box must stay uncontrolled: ECT writes into its value and disables it
|
|
196
|
+
while it loads, so do not bind `value` to your state.
|
|
197
|
+
|
|
198
|
+
## Several instances
|
|
199
|
+
|
|
200
|
+
`ino` is ECT's `data-ctw-ino`; it identifies an instance and must be unique on
|
|
201
|
+
the page. ECT keeps a single configuration per page, so the component merges the
|
|
202
|
+
settings of every mounted instance and applies what an instance sets differently
|
|
203
|
+
through `ECT.Handler.overwriteConfiguration`:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
icd11ect.coding_tool(ino="a", language="en", chapters_filter="")
|
|
207
|
+
icd11ect.coding_tool(ino="b", language="es", chapters_filter="06")
|
|
208
|
+
icd11ect.browser(ino="c", enable_select_button="categories")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Only part of ECT's settings can differ per instance: `api_server_url`,
|
|
212
|
+
`api_secured`, `source`, `minor_version`, `language`, `popup_mode`,
|
|
213
|
+
`simplified_mode`, `disable_hierarchy`, `words_available`, `chapters_available`,
|
|
214
|
+
`chapters_filter`, `subtrees_filter`, `flexisearch_available`,
|
|
215
|
+
`search_by_code_or_uri`, `hierarchy_title` and `height`
|
|
216
|
+
(`reflex_icd11ect.OVERWRITABLE_SETTINGS`). The rest is page-wide and takes the
|
|
217
|
+
value of the last instance that set it.
|
|
218
|
+
|
|
219
|
+
## Settings
|
|
220
|
+
|
|
221
|
+
Every ECT setting is a prop, snake_case instead of camelCase. Props you do not
|
|
222
|
+
set are not sent, so ECT keeps its own defaults; an empty string is treated the
|
|
223
|
+
same way, which makes state-driven props easy.
|
|
224
|
+
|
|
225
|
+
**Shared** — `api_server_url`, `api_secured`, `source` (`"mms"`, `"icf"`,
|
|
226
|
+
`"foundation"`), `minor_version`, `language`, `source_app`, `height`,
|
|
227
|
+
`hierarchy_title`, `hierarchy_resizable`, `other_postcoordination`,
|
|
228
|
+
`enable_keyboard`, `include_diagnostic_criteria`, `verbose`.
|
|
229
|
+
|
|
230
|
+
**Coding Tool** — `popup_mode`, `simplified_mode`, `disable_hierarchy`,
|
|
231
|
+
`words_available`, `chapters_available`, `chapters_filter`, `subtrees_filter`,
|
|
232
|
+
`flexisearch_available`, `search_by_code_or_uri`, `medical_coding_mode`,
|
|
233
|
+
`view_selected_uri`.
|
|
234
|
+
|
|
235
|
+
**Embedded Browser** — `enable_select_button` (`"none"`, `"categories"`,
|
|
236
|
+
`"all"`, `"allButRoot"`), `browser_search_available`,
|
|
237
|
+
`browser_advanced_search_available`, `browser_hierarchy_available`,
|
|
238
|
+
`browser_hierarchy_root_uris`, `browser_uri`,
|
|
239
|
+
`display_other_foundation_children`.
|
|
240
|
+
|
|
241
|
+
Every component accepts every setting: ECT keeps a single configuration per
|
|
242
|
+
page, so a Coding Tool setting passed to a browser would still take effect on
|
|
243
|
+
the page's Coding Tools, and hiding it would only mislead.
|
|
244
|
+
|
|
245
|
+
Bridge-only props: `token`, `token_endpoint`, `token_field`,
|
|
246
|
+
`token_timeout_ms`, `ino`, plus `placeholder`, `input_props` and `window_props`
|
|
247
|
+
for styling the elements.
|
|
248
|
+
|
|
249
|
+
`docs/settings.md` has the full table with ECT names and per-instance support.
|
|
250
|
+
|
|
251
|
+
## How it works
|
|
252
|
+
|
|
253
|
+
ECT is not a React component: it renders itself into DOM nodes carrying
|
|
254
|
+
`data-ctw-ino`, and keeps one global configuration and one set of callbacks for
|
|
255
|
+
the whole page. The component therefore:
|
|
256
|
+
|
|
257
|
+
* renders the markup ECT looks for, with your `ino`;
|
|
258
|
+
* emits a page-level runtime (`window.__reflexIcd11ect`) that owns the ECT
|
|
259
|
+
configuration and routes ECT's global callbacks to the right instance;
|
|
260
|
+
* configures ECT with `autoBind: false` and calls `ECT.Handler.bind(ino)` from a
|
|
261
|
+
`useEffect`, because ECT's auto-binding waits for `window.onload`, an event
|
|
262
|
+
that has already fired by the time a single-page app renders a route;
|
|
263
|
+
* re-applies settings and rebinds whenever your props change.
|
|
264
|
+
|
|
265
|
+
`docs/architecture.md` goes into detail.
|
|
266
|
+
|
|
267
|
+
## Demo app
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
git clone https://github.com/ecrespo/reflex-icd11ect
|
|
271
|
+
cd reflex-icd11ect
|
|
272
|
+
uv pip install -e .
|
|
273
|
+
cd icd11ect_demo && uv run reflex run
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Six pages: the Coding Tool with every setting live, the Embedded Browser,
|
|
277
|
+
several instances side by side, custom layouts, server and OAUTH 2.0 handling,
|
|
278
|
+
and a generated API reference.
|
|
279
|
+
|
|
280
|
+
## Documentation
|
|
281
|
+
|
|
282
|
+
* [`docs/settings.md`](docs/settings.md) — every prop, its ECT name and whether
|
|
283
|
+
it can differ per instance
|
|
284
|
+
* [`docs/architecture.md`](docs/architecture.md) — how the bridge works
|
|
285
|
+
* [`docs/troubleshooting.md`](docs/troubleshooting.md) — what to check when
|
|
286
|
+
nothing renders
|
|
287
|
+
* [ECT documentation](https://icd.who.int/docs/icd-api/icd11ect/) — WHO
|
|
288
|
+
* [ICD-API documentation](https://icd.who.int/docs/icd-api/) — WHO
|
|
289
|
+
* [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to set up, test and release
|
|
290
|
+
* [`SECURITY.md`](SECURITY.md) — reporting a vulnerability, and how to keep
|
|
291
|
+
the ICD-API credentials out of the browser
|
|
292
|
+
|
|
293
|
+
## Requirements
|
|
294
|
+
|
|
295
|
+
* Python >= 3.10, Reflex >= 0.9
|
|
296
|
+
* An ICD-API server (local deployment or the WHO cloud API)
|
|
297
|
+
|
|
298
|
+
## Licence
|
|
299
|
+
|
|
300
|
+
MIT for this component. The ICD-11 and the Embedded Classification Tools are
|
|
301
|
+
published by the World Health Organization under their own licence: see
|
|
302
|
+
<https://icd.who.int/en/docs/icd11-license.pdf> and the
|
|
303
|
+
[ICD-API terms](https://icd.who.int/icdapi).
|