helix.fhir.client.sdk 4.2.3__py3-none-any.whl → 4.2.19__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.
Files changed (37) hide show
  1. helix_fhir_client_sdk/fhir_auth_mixin.py +17 -10
  2. helix_fhir_client_sdk/fhir_client.py +152 -79
  3. helix_fhir_client_sdk/fhir_delete_mixin.py +62 -48
  4. helix_fhir_client_sdk/fhir_merge_mixin.py +188 -166
  5. helix_fhir_client_sdk/fhir_merge_resources_mixin.py +200 -15
  6. helix_fhir_client_sdk/fhir_patch_mixin.py +97 -84
  7. helix_fhir_client_sdk/fhir_update_mixin.py +71 -57
  8. helix_fhir_client_sdk/graph/simulated_graph_processor_mixin.py +147 -49
  9. helix_fhir_client_sdk/open_telemetry/__init__.py +0 -0
  10. helix_fhir_client_sdk/open_telemetry/attribute_names.py +7 -0
  11. helix_fhir_client_sdk/open_telemetry/span_names.py +12 -0
  12. helix_fhir_client_sdk/queue/request_queue_mixin.py +17 -12
  13. helix_fhir_client_sdk/responses/fhir_client_protocol.py +10 -6
  14. helix_fhir_client_sdk/responses/fhir_get_response.py +3 -4
  15. helix_fhir_client_sdk/responses/fhir_response_processor.py +73 -54
  16. helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py +49 -28
  17. helix_fhir_client_sdk/responses/get/fhir_get_error_response.py +0 -1
  18. helix_fhir_client_sdk/responses/get/fhir_get_list_by_resource_type_response.py +1 -1
  19. helix_fhir_client_sdk/responses/get/fhir_get_list_response.py +1 -1
  20. helix_fhir_client_sdk/responses/get/fhir_get_response_factory.py +0 -1
  21. helix_fhir_client_sdk/responses/get/fhir_get_single_response.py +1 -1
  22. helix_fhir_client_sdk/responses/merge/fhir_merge_resource_response_entry.py +30 -0
  23. helix_fhir_client_sdk/responses/resource_separator.py +35 -40
  24. helix_fhir_client_sdk/utilities/cache/request_cache.py +32 -43
  25. helix_fhir_client_sdk/utilities/retryable_aiohttp_client.py +185 -154
  26. helix_fhir_client_sdk/utilities/retryable_aiohttp_response.py +2 -1
  27. helix_fhir_client_sdk/validators/async_fhir_validator.py +3 -0
  28. helix_fhir_client_sdk-4.2.19.dist-info/METADATA +200 -0
  29. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/RECORD +36 -29
  30. tests/async/test_benchmark_compress.py +448 -0
  31. tests/async/test_benchmark_merge.py +506 -0
  32. tests/async/test_retryable_client_session_management.py +159 -0
  33. tests/test_fhir_client_clone.py +155 -0
  34. helix_fhir_client_sdk-4.2.3.dist-info/METADATA +0 -115
  35. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/WHEEL +0 -0
  36. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/licenses/LICENSE +0 -0
  37. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,448 @@
1
+ """
2
+ Benchmark tests for comparing compressed vs uncompressed FHIR client operations.
3
+
4
+ These tests measure the performance of:
5
+ - get_async() with compress=True vs compress=False
6
+ - get_raw_resources_async() with compress=True vs compress=False
7
+
8
+ =============================================================================
9
+ HOW TO RUN THESE TESTS
10
+ =============================================================================
11
+
12
+ 1. Start services using docker-compose:
13
+ docker-compose up -d mock-server
14
+
15
+ 2. First time only - rebuild dev container to include pytest-benchmark:
16
+ docker-compose build dev
17
+
18
+ OR install pytest-benchmark in the running container:
19
+ docker-compose run --rm dev pip install pytest-benchmark
20
+
21
+ 3. Run benchmark tests inside docker container:
22
+ docker-compose run --rm dev pytest tests/async/test_benchmark_compress.py -v --benchmark-only
23
+
24
+ 4. Or run all benchmark variations:
25
+ docker-compose run --rm dev pytest tests/async/test_benchmark_compress.py -v --benchmark-only --benchmark-group-by=func
26
+
27
+ 5. Save benchmark results for comparison:
28
+ docker-compose run --rm dev pytest tests/async/test_benchmark_compress.py -v --benchmark-autosave
29
+
30
+ 6. Compare with previous runs:
31
+ docker-compose run --rm dev pytest tests/async/test_benchmark_compress.py -v --benchmark-compare
32
+
33
+ 7. Run with more iterations for accuracy:
34
+ docker-compose run --rm dev pytest tests/async/test_benchmark_compress.py -v --benchmark-min-rounds=10
35
+
36
+ 8. To stop mock-server:
37
+ docker-compose down mock-server
38
+
39
+ =============================================================================
40
+ """
41
+
42
+ import asyncio
43
+ import json
44
+ import socket
45
+ from typing import Any
46
+
47
+ import pytest
48
+ from mockserver_client.mockserver_client import (
49
+ MockServerFriendlyClient,
50
+ mock_request,
51
+ mock_response,
52
+ times,
53
+ )
54
+
55
+ from helix_fhir_client_sdk.fhir_client import FhirClient
56
+ from helix_fhir_client_sdk.responses.fhir_get_response import FhirGetResponse
57
+
58
+
59
+ def is_mock_server_running(host: str = "mock-server", port: int = 1080) -> bool:
60
+ """Check if mock-server is reachable."""
61
+ try:
62
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
63
+ sock.settimeout(2)
64
+ result = sock.connect_ex((host, port))
65
+ sock.close()
66
+ return result == 0
67
+ except OSError:
68
+ return False
69
+
70
+
71
+ # Skip all tests if mock-server is not running
72
+ pytestmark = pytest.mark.skipif(
73
+ not is_mock_server_running(), reason="Mock server not running. Start with: docker-compose up -d mock-server"
74
+ )
75
+
76
+
77
+ def generate_patient_resource(index: int) -> dict[str, Any]:
78
+ """Generate a realistic FHIR Patient resource."""
79
+ return {
80
+ "resourceType": "Patient",
81
+ "id": f"patient-{index}",
82
+ "meta": {
83
+ "versionId": "1",
84
+ "lastUpdated": "2025-01-15T10:30:00.000Z",
85
+ "source": "http://example.org/fhir",
86
+ "profile": ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"],
87
+ },
88
+ "identifier": [
89
+ {
90
+ "use": "official",
91
+ "type": {
92
+ "coding": [
93
+ {
94
+ "system": "http://terminology.hl7.org/CodeSystem/v2-0203",
95
+ "code": "MR",
96
+ "display": "Medical Record Number",
97
+ }
98
+ ]
99
+ },
100
+ "system": "http://hospital.example.org/mrn",
101
+ "value": f"MRN-{index:08d}",
102
+ },
103
+ {
104
+ "use": "official",
105
+ "type": {
106
+ "coding": [
107
+ {
108
+ "system": "http://terminology.hl7.org/CodeSystem/v2-0203",
109
+ "code": "SS",
110
+ "display": "Social Security Number",
111
+ }
112
+ ]
113
+ },
114
+ "system": "http://hl7.org/fhir/sid/us-ssn",
115
+ "value": f"{100 + index:03d}-{50 + index:02d}-{1000 + index:04d}",
116
+ },
117
+ ],
118
+ "active": True,
119
+ "name": [
120
+ {
121
+ "use": "official",
122
+ "family": f"TestFamily{index}",
123
+ "given": [f"TestGiven{index}", f"MiddleName{index}"],
124
+ "prefix": ["Mr."],
125
+ "suffix": ["Jr."],
126
+ },
127
+ {
128
+ "use": "nickname",
129
+ "given": [f"Nick{index}"],
130
+ },
131
+ ],
132
+ "telecom": [
133
+ {"system": "phone", "value": f"555-{100 + index:03d}-{1000 + index:04d}", "use": "home"},
134
+ {"system": "phone", "value": f"555-{200 + index:03d}-{2000 + index:04d}", "use": "mobile"},
135
+ {"system": "email", "value": f"patient{index}@example.com", "use": "home"},
136
+ ],
137
+ "gender": "male" if index % 2 == 0 else "female",
138
+ "birthDate": f"{1950 + (index % 50)}-{(index % 12) + 1:02d}-{(index % 28) + 1:02d}",
139
+ "deceasedBoolean": False,
140
+ "address": [
141
+ {
142
+ "use": "home",
143
+ "type": "physical",
144
+ "line": [f"{100 + index} Main Street", f"Apt {index}"],
145
+ "city": "Boston",
146
+ "state": "MA",
147
+ "postalCode": f"02{100 + (index % 900):03d}",
148
+ "country": "USA",
149
+ },
150
+ {
151
+ "use": "work",
152
+ "type": "postal",
153
+ "line": [f"{200 + index} Business Ave"],
154
+ "city": "Cambridge",
155
+ "state": "MA",
156
+ "postalCode": f"02{200 + (index % 800):03d}",
157
+ "country": "USA",
158
+ },
159
+ ],
160
+ "maritalStatus": {
161
+ "coding": [
162
+ {
163
+ "system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus",
164
+ "code": "M" if index % 2 == 0 else "S",
165
+ "display": "Married" if index % 2 == 0 else "Never Married",
166
+ }
167
+ ]
168
+ },
169
+ "communication": [
170
+ {
171
+ "language": {
172
+ "coding": [
173
+ {
174
+ "system": "urn:ietf:bcp:47",
175
+ "code": "en-US",
176
+ "display": "English (United States)",
177
+ }
178
+ ]
179
+ },
180
+ "preferred": True,
181
+ }
182
+ ],
183
+ "generalPractitioner": [{"reference": f"Practitioner/practitioner-{index % 10}"}],
184
+ "managingOrganization": {"reference": "Organization/org-1"},
185
+ }
186
+
187
+
188
+ def generate_patient_bundle(count: int) -> dict[str, Any]:
189
+ """Generate a FHIR Bundle with multiple Patient resources."""
190
+ entries = []
191
+ for i in range(count):
192
+ entries.append(
193
+ {
194
+ "fullUrl": f"http://example.org/fhir/Patient/patient-{i}",
195
+ "resource": generate_patient_resource(i),
196
+ "search": {"mode": "match"},
197
+ }
198
+ )
199
+ return {
200
+ "resourceType": "Bundle",
201
+ "id": "bundle-search-result",
202
+ "type": "searchset",
203
+ "total": count,
204
+ "link": [
205
+ {"relation": "self", "url": f"http://example.org/fhir/Patient?_count={count}"},
206
+ ],
207
+ "entry": entries,
208
+ }
209
+
210
+
211
+ @pytest.fixture(scope="module")
212
+ def mock_server_url() -> str:
213
+ return "http://mock-server:1080"
214
+
215
+
216
+ @pytest.fixture(scope="module")
217
+ def mock_client(mock_server_url: str) -> MockServerFriendlyClient:
218
+ return MockServerFriendlyClient(base_url=mock_server_url)
219
+
220
+
221
+ @pytest.fixture(scope="module")
222
+ def setup_mock_endpoints(mock_client: MockServerFriendlyClient, mock_server_url: str) -> str:
223
+ """Set up mock endpoints for different payload sizes."""
224
+ test_name = "benchmark_compress"
225
+
226
+ mock_client.clear(f"/{test_name}/*.*")
227
+ mock_client.reset()
228
+
229
+ # Create payloads of different sizes for benchmarking
230
+ payloads = {
231
+ "small": generate_patient_bundle(10), # ~10KB
232
+ "medium": generate_patient_bundle(100), # ~100KB
233
+ "large": generate_patient_bundle(500), # ~500KB
234
+ }
235
+
236
+ # Setup mock endpoints for each payload size
237
+ for size, bundle in payloads.items():
238
+ response_body = json.dumps(bundle)
239
+ # Endpoint for GET /Patient (returns bundle)
240
+ mock_client.expect(
241
+ request=mock_request(path=f"/{test_name}/{size}/Patient", method="GET"),
242
+ response=mock_response(body=response_body),
243
+ timing=times(10000), # Allow many requests for benchmarking
244
+ )
245
+ # Endpoint for GET /Patient/{id} (returns single resource)
246
+ mock_client.expect(
247
+ request=mock_request(path=f"/{test_name}/{size}/Patient/{size}", method="GET"),
248
+ response=mock_response(body=response_body),
249
+ timing=times(10000),
250
+ )
251
+
252
+ return f"{mock_server_url}/{test_name}"
253
+
254
+
255
+ # ============================================================================
256
+ # Benchmark Tests for get_async()
257
+ # ============================================================================
258
+
259
+
260
+ def test_benchmark_get_async_compress_false_small(benchmark: Any, setup_mock_endpoints: str) -> None:
261
+ """Benchmark get_async with compress=False and a small payload (10 patients)."""
262
+ base_url = f"{setup_mock_endpoints}/small"
263
+
264
+ async def run_get_async() -> FhirGetResponse:
265
+ fhir_client = FhirClient().url(base_url).resource("Patient")
266
+ return await fhir_client.compress(False).get_async()
267
+
268
+ def run_sync() -> FhirGetResponse:
269
+ return asyncio.run(run_get_async())
270
+
271
+ result = benchmark(run_sync)
272
+ assert result is not None
273
+ assert result.get_response_text() is not None
274
+
275
+
276
+ def test_benchmark_get_async_compress_true_small(benchmark: Any, setup_mock_endpoints: str) -> None:
277
+ """Benchmark get_async with compress=True and a small payload (10 patients)."""
278
+ base_url = f"{setup_mock_endpoints}/small"
279
+
280
+ async def run_get_async() -> FhirGetResponse:
281
+ fhir_client = FhirClient().url(base_url).resource("Patient")
282
+ return await fhir_client.compress(True).get_async()
283
+
284
+ def run_sync() -> FhirGetResponse:
285
+ return asyncio.run(run_get_async())
286
+
287
+ result = benchmark(run_sync)
288
+ assert result is not None
289
+ assert result.get_response_text() is not None
290
+
291
+
292
+ def test_benchmark_get_async_compress_false_medium(benchmark: Any, setup_mock_endpoints: str) -> None:
293
+ """Benchmark get_async with compress=False and medium payload (100 patients)."""
294
+ base_url = f"{setup_mock_endpoints}/medium"
295
+
296
+ async def run_get_async() -> FhirGetResponse:
297
+ fhir_client = FhirClient().url(base_url).resource("Patient")
298
+ return await fhir_client.compress(False).get_async()
299
+
300
+ def run_sync() -> FhirGetResponse:
301
+ return asyncio.run(run_get_async())
302
+
303
+ result = benchmark(run_sync)
304
+ assert result is not None
305
+ assert result.get_response_text() is not None
306
+
307
+
308
+ def test_benchmark_get_async_compress_true_medium(benchmark: Any, setup_mock_endpoints: str) -> None:
309
+ """Benchmark get_async with compress=True and medium payload (100 patients)."""
310
+ base_url = f"{setup_mock_endpoints}/medium"
311
+
312
+ async def run_get_async() -> FhirGetResponse:
313
+ fhir_client = FhirClient().url(base_url).resource("Patient")
314
+ return await fhir_client.compress(True).get_async()
315
+
316
+ def run_sync() -> FhirGetResponse:
317
+ return asyncio.run(run_get_async())
318
+
319
+ result = benchmark(run_sync)
320
+ assert result is not None
321
+ assert result.get_response_text() is not None
322
+
323
+
324
+ def test_benchmark_get_async_compress_false_large(benchmark: Any, setup_mock_endpoints: str) -> None:
325
+ """Benchmark get_async with compress=False and a large payload (500 patients)."""
326
+ base_url = f"{setup_mock_endpoints}/large"
327
+
328
+ async def run_get_async() -> FhirGetResponse:
329
+ fhir_client = FhirClient().url(base_url).resource("Patient")
330
+ return await fhir_client.compress(False).get_async()
331
+
332
+ def run_sync() -> FhirGetResponse:
333
+ return asyncio.run(run_get_async())
334
+
335
+ result = benchmark(run_sync)
336
+ assert result is not None
337
+ assert result.get_response_text() is not None
338
+
339
+
340
+ def test_benchmark_get_async_compress_true_large(benchmark: Any, setup_mock_endpoints: str) -> None:
341
+ """Benchmark get_async with compress=True and a large payload (500 patients)."""
342
+ base_url = f"{setup_mock_endpoints}/large"
343
+
344
+ async def run_get_async() -> FhirGetResponse:
345
+ fhir_client = FhirClient().url(base_url).resource("Patient")
346
+ return await fhir_client.compress(True).get_async()
347
+
348
+ def run_sync() -> FhirGetResponse:
349
+ return asyncio.run(run_get_async())
350
+
351
+ result = benchmark(run_sync)
352
+ assert result is not None
353
+ assert result.get_response_text() is not None
354
+
355
+
356
+ # ============================================================================
357
+ # Benchmark Tests for get_raw_resources_async()
358
+ # ============================================================================
359
+
360
+
361
+ def test_benchmark_get_raw_resources_async_compress_false_small(benchmark: Any, setup_mock_endpoints: str) -> None:
362
+ """Benchmark get_raw_resources_async with compress=False and small payload."""
363
+ base_url = f"{setup_mock_endpoints}/small"
364
+
365
+ async def run_get_raw() -> dict[str, Any]:
366
+ fhir_client = FhirClient().url(base_url).resource("Patient")
367
+ return await fhir_client.compress(False).get_raw_resources_async()
368
+
369
+ def run_sync() -> dict[str, Any]:
370
+ return asyncio.run(run_get_raw())
371
+
372
+ result = benchmark(run_sync)
373
+ assert result is not None
374
+
375
+
376
+ def test_benchmark_get_raw_resources_async_compress_true_small(benchmark: Any, setup_mock_endpoints: str) -> None:
377
+ """Benchmark get_raw_resources_async with compress=True and a small payload."""
378
+ base_url = f"{setup_mock_endpoints}/small"
379
+
380
+ async def run_get_raw() -> dict[str, Any]:
381
+ fhir_client = FhirClient().url(base_url).resource("Patient")
382
+ return await fhir_client.compress(True).get_raw_resources_async()
383
+
384
+ def run_sync() -> dict[str, Any]:
385
+ return asyncio.run(run_get_raw())
386
+
387
+ result = benchmark(run_sync)
388
+ assert result is not None
389
+
390
+
391
+ def test_benchmark_get_raw_resources_async_compress_false_medium(benchmark: Any, setup_mock_endpoints: str) -> None:
392
+ """Benchmark get_raw_resources_async with compress=False and medium payload."""
393
+ base_url = f"{setup_mock_endpoints}/medium"
394
+
395
+ async def run_get_raw() -> dict[str, Any]:
396
+ fhir_client = FhirClient().url(base_url).resource("Patient")
397
+ return await fhir_client.compress(False).get_raw_resources_async()
398
+
399
+ def run_sync() -> dict[str, Any]:
400
+ return asyncio.run(run_get_raw())
401
+
402
+ result = benchmark(run_sync)
403
+ assert result is not None
404
+
405
+
406
+ def test_benchmark_get_raw_resources_async_compress_true_medium(benchmark: Any, setup_mock_endpoints: str) -> None:
407
+ """Benchmark get_raw_resources_async with compress=True and medium payload."""
408
+ base_url = f"{setup_mock_endpoints}/medium"
409
+
410
+ async def run_get_raw() -> dict[str, Any]:
411
+ fhir_client = FhirClient().url(base_url).resource("Patient")
412
+ return await fhir_client.compress(True).get_raw_resources_async()
413
+
414
+ def run_sync() -> dict[str, Any]:
415
+ return asyncio.run(run_get_raw())
416
+
417
+ result = benchmark(run_sync)
418
+ assert result is not None
419
+
420
+
421
+ def test_benchmark_get_raw_resources_async_compress_false_large(benchmark: Any, setup_mock_endpoints: str) -> None:
422
+ """Benchmark get_raw_resources_async with compress=False and a large payload."""
423
+ base_url = f"{setup_mock_endpoints}/large"
424
+
425
+ async def run_get_raw() -> dict[str, Any]:
426
+ fhir_client = FhirClient().url(base_url).resource("Patient")
427
+ return await fhir_client.compress(False).get_raw_resources_async()
428
+
429
+ def run_sync() -> dict[str, Any]:
430
+ return asyncio.run(run_get_raw())
431
+
432
+ result = benchmark(run_sync)
433
+ assert result is not None
434
+
435
+
436
+ def test_benchmark_get_raw_resources_async_compress_true_large(benchmark: Any, setup_mock_endpoints: str) -> None:
437
+ """Benchmark get_raw_resources_async with compress=True and a large payload."""
438
+ base_url = f"{setup_mock_endpoints}/large"
439
+
440
+ async def run_get_raw() -> dict[str, Any]:
441
+ fhir_client = FhirClient().url(base_url).resource("Patient")
442
+ return await fhir_client.compress(True).get_raw_resources_async()
443
+
444
+ def run_sync() -> dict[str, Any]:
445
+ return asyncio.run(run_get_raw())
446
+
447
+ result = benchmark(run_sync)
448
+ assert result is not None