fhir-pyrate 0.2.0b9__py3-none-any.whl → 0.2.2__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.
@@ -1,10 +1,12 @@
1
1
  import logging
2
- from datetime import datetime, timedelta
2
+ from datetime import timedelta
3
3
  from typing import Any, Optional, Union
4
4
 
5
5
  import jwt
6
6
  import requests
7
7
 
8
+ from fhir_pyrate.util import now_utc
9
+
8
10
  logger = logging.getLogger(__name__)
9
11
 
10
12
 
@@ -35,10 +37,10 @@ class TokenAuth(requests.auth.AuthBase):
35
37
  username: str,
36
38
  password: str,
37
39
  auth_url: str,
38
- refresh_url: str = None,
39
- session: requests.Session = None,
40
+ refresh_url: Optional[str] = None,
41
+ session: Optional[requests.Session] = None,
40
42
  max_login_attempts: int = 5,
41
- token_refresh_delta: Union[int, timedelta] = None,
43
+ token_refresh_delta: Optional[Union[int, timedelta]] = None,
42
44
  ) -> None:
43
45
  self._username = username
44
46
  self._password = password
@@ -63,11 +65,11 @@ class TokenAuth(requests.auth.AuthBase):
63
65
  )
64
66
  self.token: Optional[str] = None
65
67
  self._authenticate()
66
- self.auth_time = datetime.now()
68
+ self.auth_time = now_utc()
67
69
 
68
70
  def _authenticate(self) -> None:
69
71
  """
70
- Authenticates the user using the authentication URL and sets the token.
72
+ Authenticate the user using the authentication URL and sets the token.
71
73
  """
72
74
  # Authentication to get the token
73
75
  response = self._token_session.get(
@@ -78,7 +80,7 @@ class TokenAuth(requests.auth.AuthBase):
78
80
 
79
81
  def __call__(self, r: requests.PreparedRequest) -> requests.PreparedRequest:
80
82
  """
81
- Sets the necessary authentication header of the current request.
83
+ Set the necessary authentication header of the current request.
82
84
 
83
85
  :param r: The prepared request that should be sent
84
86
  :return: The prepared request
@@ -88,7 +90,7 @@ class TokenAuth(requests.auth.AuthBase):
88
90
 
89
91
  def is_refresh_required(self) -> bool:
90
92
  """
91
- Computes whether the token should be refreshed according to the given token and to the
93
+ Compute whether the token should be refreshed according to the given token and to the
92
94
  _token_refresh_delta variable.
93
95
 
94
96
  :return: Whether the token is about to expire and should thus be refreshed
@@ -109,7 +111,7 @@ class TokenAuth(requests.auth.AuthBase):
109
111
  )
110
112
  # If there is no expiration time return False
111
113
  # If we are already in the last 25% of the time return True
112
- return refresh_interval is not None and datetime.now().timestamp() > (
114
+ return refresh_interval is not None and now_utc().timestamp() > (
113
115
  decoded.get("exp") - refresh_interval
114
116
  )
115
117
  except jwt.exceptions.PyJWTError:
@@ -118,10 +120,10 @@ class TokenAuth(requests.auth.AuthBase):
118
120
  # If it has been specified and the time is almost run out
119
121
  return (
120
122
  self._token_refresh_delta is not None
121
- and (datetime.now() - self.auth_time) > self._token_refresh_delta
123
+ and (now_utc() - self.auth_time) > self._token_refresh_delta
122
124
  )
123
125
 
124
- def refresh_token(self, token: str = None) -> None:
126
+ def refresh_token(self, token: Optional[str] = None) -> None:
125
127
  """
126
128
  Refresh the current session either by logging in again or by refreshing the token.
127
129
 
@@ -139,16 +141,16 @@ class TokenAuth(requests.auth.AuthBase):
139
141
  else:
140
142
  response.raise_for_status()
141
143
  self.token = response.text
142
- self.auth_time = datetime.now()
144
+ self.auth_time = now_utc()
143
145
  else:
144
146
  self._authenticate()
145
- self.auth_time = datetime.now()
147
+ self.auth_time = now_utc()
146
148
 
147
149
  def _refresh_hook(
148
150
  self, response: requests.Response, *args: Any, **kwargs: Any
149
151
  ) -> Optional[requests.Response]:
150
152
  """
151
- Hook that is called after every request, it checks whether the login was successful and
153
+ Check whether the login was successful and
152
154
  if it was not, it either refreshes the token or authenticates the user again.
153
155
 
154
156
  :param response: The received response
@@ -164,15 +166,17 @@ class TokenAuth(requests.auth.AuthBase):
164
166
  # If the state is unauthorized,
165
167
  # then we should set how many times we have tried logging in
166
168
  if response.status_code == requests.codes.unauthorized:
167
- if hasattr(response.request, "login_reattempted_times"):
168
- response.request.login_reattempted_times += 1 # type: ignore
169
- if (
170
- response.request.login_reattempted_times # type: ignore
171
- >= self._max_login_attempts
172
- ):
173
- response.raise_for_status()
174
- else:
175
- response.request.login_reattempted_times = 1 # type: ignore
169
+ login_attempts: int = getattr(
170
+ response.request, "login_reattempted_times", 0
171
+ )
172
+ logger.info("Refreshing token because of unauthorized status.")
173
+ login_attempts += 1
174
+ if login_attempts >= self._max_login_attempts:
175
+ response.raise_for_status()
176
+ setattr(response.request, "login_reattempted_times", login_attempts) # noqa
177
+ else:
178
+ logger.info("Refreshing token refresh is required.")
179
+
176
180
  # If the token is None, then we were never actually authenticated
177
181
  if self.token is None:
178
182
  response.raise_for_status()
fhir_pyrate/util/util.py CHANGED
@@ -1,9 +1,13 @@
1
- import datetime
1
+ from datetime import datetime, timezone
2
2
  from typing import Any
3
3
 
4
4
  import pandas as pd
5
5
 
6
6
 
7
+ def now_utc() -> datetime:
8
+ return datetime.now(timezone.utc)
9
+
10
+
7
11
  def string_from_column(
8
12
  col: pd.Series,
9
13
  separator: str = ", ",
@@ -12,7 +16,7 @@ def string_from_column(
12
16
  sort_reverse: bool = False,
13
17
  ) -> Any:
14
18
  """
15
- Transforms the values contained in a pandas Series into a string of (if desired unique) values.
19
+ Transform the values contained in a pandas Series into a string of (if desired unique) values.
16
20
 
17
21
  :param col:
18
22
  :param separator: The separator for the values
@@ -21,7 +25,7 @@ def string_from_column(
21
25
  :param sort_reverse: Whether the values should sorted in reverse order
22
26
  :return: A string containing the values of the Series.
23
27
  """
24
- existing_values = list()
28
+ existing_values = []
25
29
  for el in col.values:
26
30
  if not pd.isnull(el) and el != "":
27
31
  existing_values.append(el)
@@ -40,9 +44,9 @@ def string_from_column(
40
44
 
41
45
  def get_datetime(dt_format: str = "%Y-%m-%d %H:%M:%S") -> str:
42
46
  """
43
- Creates a datetime string according to the given format
47
+ Create a datetime string according to the given format
44
48
 
45
49
  :param dt_format: The format to use for the printing
46
50
  :return: The formatted string
47
51
  """
48
- return datetime.datetime.now().strftime(dt_format)
52
+ return datetime.now().strftime(dt_format)
@@ -1,28 +1,28 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fhir-pyrate
3
- Version: 0.2.0b9
3
+ Version: 0.2.2
4
4
  Summary: FHIR-PYrate is a package that provides a high-level API to query FHIR Servers for bundles of resources and return the structured information as pandas DataFrames. It can also be used to filter resources using RegEx and SpaCy and download DICOM studies and series.
5
5
  Home-page: https://github.com/UMEssen/FHIR-PYrate
6
6
  License: MIT
7
7
  Keywords: python,fhir,data-science,fhirpath,healthcare
8
8
  Author: Rene Hosch
9
9
  Author-email: rene.hosch@uk-essen.de
10
- Requires-Python: >=3.8,<4.0
10
+ Requires-Python: >=3.10,<4.0
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.8
14
- Classifier: Programming Language :: Python :: 3.9
15
13
  Classifier: Programming Language :: Python :: 3.10
16
14
  Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
17
  Provides-Extra: all
18
18
  Provides-Extra: downloader
19
19
  Provides-Extra: miner
20
20
  Requires-Dist: PyJWT (>=2.4.0,<3.0.0)
21
21
  Requires-Dist: SimpleITK (>=2.0.2,<3.0.0) ; extra == "downloader" or extra == "all"
22
22
  Requires-Dist: dicomweb-client (>=0.52.0,<0.53.0) ; extra == "downloader" or extra == "all"
23
- Requires-Dist: fhirpathpy (>=0.1.0,<0.2.0)
24
- Requires-Dist: numpy (>=1.22,<2.0)
25
- Requires-Dist: pandas (>=1.3.0,<2.0.0)
23
+ Requires-Dist: fhirpathpy (>=0.2.2,<0.3.0)
24
+ Requires-Dist: numpy (>=2.0.0,<3.0.0)
25
+ Requires-Dist: pandas (>=2.0.0,<3.0.0)
26
26
  Requires-Dist: pydicom (>=2.1.2,<3.0.0) ; extra == "downloader" or extra == "all"
27
27
  Requires-Dist: requests (>=2.28.0,<3.0.0)
28
28
  Requires-Dist: requests-cache (>=0.9.7,<0.10.0)
@@ -32,22 +32,22 @@ Project-URL: Repository, https://github.com/UMEssen/FHIR-PYrate
32
32
  Description-Content-Type: text/markdown
33
33
 
34
34
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
- [![Supported Python version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/release/python-380/)
35
+ [![Supported Python version](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/release/python-31011/)
36
36
  [![Stable Version](https://img.shields.io/pypi/v/fhir-pyrate?label=stable)](https://pypi.org/project/fhir-pyrate/)
37
37
  [![Pre-release Version](https://img.shields.io/github/v/release/UMEssen/fhir-pyrate?label=pre-release&include_prereleases&sort=semver)](https://pypi.org/project/fhir-pyrate/#history)
38
38
  [![DOI](https://zenodo.org/badge/456893108.svg)](https://zenodo.org/badge/latestdoi/456893108)
39
+ [![Affiliated with RTG WisPerMed](https://img.shields.io/badge/Affiliated-RTG%202535%20WisPerMed-blue)](https://wispermed.org/)
39
40
 
40
41
  <!-- PROJECT LOGO -->
41
- <br />
42
- <div align="center">
43
- <a href="https://github.com/UMEssen/FHIR-PYrate">
44
- <img src="https://raw.githubusercontent.com/UMEssen/FHIR-PYrate/main/images/logo.svg" alt="Logo" width="440" height="338">
45
- </a>
46
- </div>
42
+ ![Pyrate-Banner](images/pyrate-banner.png)
47
43
 
48
44
  This package is meant to provide a simple abstraction to query and structure FHIR resources as
49
45
  pandas DataFrames. Want to use R instead? Try out [fhircrackr](https://github.com/POLAR-fhiR/fhircrackr)!
50
46
 
47
+ **If you use this package, please cite:**
48
+
49
+ Hosch, R., Baldini, G., Parmar, V. et al. FHIR-PYrate: a data science friendly Python package to query FHIR servers. BMC Health Serv Res 23, 734 (2023). https://doi.org/10.1186/s12913-023-09498-1
50
+
51
51
  There are four main classes:
52
52
  * [Ahoy](https://github.com/UMEssen/FHIR-PYrate/blob/main/fhir_pyrate/ahoy.py): Authenticate on the FHIR API
53
53
  ([Example 1](https://github.com/UMEssen/FHIR-PYrate/blob/main/examples/1-simple-json-to-df.ipynb),
@@ -71,11 +71,6 @@ our institute. If there is anything in the code that only applies to our server,
71
71
  problems with the authentication (or anything else really), please just create an issue or
72
72
  [email us](mailto:giulia.baldini@uk-essen.de).
73
73
 
74
- <br />
75
- <div align="center">
76
- <img src="https://raw.githubusercontent.com/UMEssen/FHIR-PYrate/main/images/resources.svg" alt="Resources" width="630" height="385">
77
- </div>
78
-
79
74
  <!-- TABLE OF CONTENTS -->
80
75
  Table of Contents:
81
76
 
@@ -236,10 +231,12 @@ The Pirate functions do one of three things:
236
231
  | trade_rows_for_dataframe | 3 | Yes | Yes | DataFrame |
237
232
 
238
233
 
239
- **BETA FEATURE**: It is also possible to cache the bundles using the `bundle_caching` parameter,
240
- which specifies a caching folder. This has not yet been tested extensively and does not have any
241
- cache invalidation mechanism.
242
-
234
+ **CACHING**: It is also possible to cache the bundles using the `cache_folder` parameter.
235
+ This unfortunately does not currently work with multiprocessing, but saves a lot of time if you
236
+ need to download a lot of data and you are always doing the same requests.
237
+ You can also specify how long the cache should be valid with the `cache_expiry_time` parameter.
238
+ Additionally, you can also specify whether the requests should be retried using the `retry_requests`
239
+ parameter. There is an example of this in the docstrings of the Pirate class.
243
240
 
244
241
  A toy request for ImagingStudy:
245
242
 
@@ -429,7 +426,65 @@ parameters specified in `df_constraints` as columns of the final DataFrame.
429
426
  You can find an example in [Example 3](https://github.com/UMEssen/FHIR-PYrate/blob/main/examples/3-patients-for-condition.ipynb).
430
427
  Additionally, you can specify the `with_columns` parameter, which can add any columns from the original
431
428
  DataFrame. The columns can be either specified as a list of columns `[col1, col2, ...]` or as a
432
- list of tuples `[(new_name_for_col1, col1), (new_name_for_col2, col2), ...]`
429
+ list of tuples `[(new_name_for_col1, col1), (new_name_for_col2, col2), ...]`.
430
+
431
+ Currently, whenever a column is completely empty (i.e., no resources
432
+ have a corresponding value for that column), it is just removed from the DataFrame.
433
+ This is to ensure that we output clean DataFrames when we are handling multiple resources.
434
+ More on that in the following section.
435
+
436
+ #### Note on Querying Multiple Resources
437
+
438
+ Not all FHIR servers allow this (at least not the public ones that we have tried),
439
+ but it is also possible to obtain multiple resources with just one query:
440
+ ```python
441
+ search = ...
442
+ result_dfs = search.steal_bundles_to_dataframe(
443
+ resource_type="ImagingStudy",
444
+ request_params={
445
+ "_lastUpdated": "ge2022-12",
446
+ "_count": "3",
447
+ "_include": "ImagingStudy:subject",
448
+ },
449
+ fhir_paths=[
450
+ "id",
451
+ "started",
452
+ ("modality", "modality.code"),
453
+ ("procedureCode", "procedureCode.coding.code"),
454
+ (
455
+ "study_instance_uid",
456
+ "identifier.where(system = 'urn:dicom:uid').value.replace('urn:oid:', '')",
457
+ ),
458
+ ("series_instance_uid", "series.uid"),
459
+ ("series_code", "series.modality.code"),
460
+ ("numberOfInstances", "series.numberOfInstances"),
461
+ ("family_first", "name[0].family"),
462
+ ("given_first", "name[0].given"),
463
+ ],
464
+ num_pages=1,
465
+ )
466
+ ```
467
+ In this case, a dictionary of DataFrames is returned, where the keys are the resource types.
468
+ You can then select the single dictionary by doing `result_dfs["ImagingStudy"]`
469
+ or `result_dfs["Patient"]`.
470
+ You can find an example of this in [Example 2](https://github.com/UMEssen/FHIR-PYrate/blob/main/examples/2-condition-to-imaging-study.ipynb)
471
+ where the `ImagingStudy` resource is queried.
472
+
473
+ In theory, it would be smarter to specify the resource name in front of the FHIRPaths,
474
+ e.g. `ImagingStudy.series.uid` instead of `series.uid`, and for each DataFrame only return the
475
+ corresponding attributes.
476
+ However, we do not want to force the user to always specify the resource type, and in the current
477
+ version the DataFrames
478
+ coming from multiple resources have the same columns, because
479
+ we cannot filter which resource was actually intended.
480
+ Currently, we solved this by just removing all columns that do not have any results.
481
+ Which means however, that if you are actually requesting an attribute for a specific resource and it
482
+ is not found, that that column will not appear.
483
+ In the future, [we plan to do a smarter filtering of the FHIRPaths](https://github.com/UMEssen/FHIR-PYrate/issues/120),
484
+ such that only the ones containing
485
+ the actual resource name are kept if the resource name is specified in the path,
486
+ and that a column full of `None`s is obtained in case no resource type is specified.
487
+
433
488
 
434
489
  ### [Miner](https://github.com/UMEssen/FHIR-PYrate/blob/main/fhir_pyrate/miner.py)
435
490
 
@@ -0,0 +1,15 @@
1
+ fhir_pyrate/__init__.py,sha256=GrVFlghs2Q8pGCVLdNjD40R0Xt8ptvF5NhCtT1FUazk,864
2
+ fhir_pyrate/ahoy.py,sha256=dOw94Khg3_4fXnJHAnZFlazxy_Tbb32Yi0HGOYVaHLU,5611
3
+ fhir_pyrate/dicom_downloader.py,sha256=GNoOSlyt_pdykdcfvHS287iXMrVBHeyY1WIeegHnbvk,28215
4
+ fhir_pyrate/miner.py,sha256=wydCL6zmVHSf4ctwz2760mZAfe9byVQciUWUQyRHVtQ,7331
5
+ fhir_pyrate/pirate.py,sha256=xNqmz5g5EnB3-RLZM46JYXkoGISFVmYEQUg2M-bTcKA,70318
6
+ fhir_pyrate/util/__init__.py,sha256=-P4jBpsH6XzQELrLAod2_P9oBocfaYUOWb_LQ-QjyKI,193
7
+ fhir_pyrate/util/bundle_processing_templates.py,sha256=EERL26gbv2hkYCoxWj1ZguJK5TKJ3e03zMAJuKSkczY,4519
8
+ fhir_pyrate/util/fhirobj.py,sha256=nTkSUbsmOisgDDgD_cEggF8hlPVjm7CWVSrkA8dOq3E,903
9
+ fhir_pyrate/util/imports.py,sha256=3s0hvuonX_susm5anw4fZORh7V3JMJ4hoLPBVSoj7Lw,4333
10
+ fhir_pyrate/util/token_auth.py,sha256=mfLzlZxCnWbiOOMZh_IDYgREp_9fhI3FshB8mDULtaQ,7862
11
+ fhir_pyrate/util/util.py,sha256=1qIdoriWNksD3DRYm7qSRCli5t9524wa3mVf21fETgs,1507
12
+ fhir_pyrate-0.2.2.dist-info/LICENSE,sha256=1IS7y51_JRtpCVVbUpYyztTMj3A6FskEZJBQkAQ9w9o,1084
13
+ fhir_pyrate-0.2.2.dist-info/METADATA,sha256=V5L-hgjfdmrikX1nmZNsBHbf1utdOj-zSsQfKeV6ybQ,29679
14
+ fhir_pyrate-0.2.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
15
+ fhir_pyrate-0.2.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.5.1
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,15 +0,0 @@
1
- fhir_pyrate/__init__.py,sha256=-25N2f8w3P0YXynSIqZcp2naR5lq0Q4_Xag2TZ4Ln7g,864
2
- fhir_pyrate/ahoy.py,sha256=E5P_V27kqRbuPsnI7pmH8sTmREwagSjbiVIw3BuotW8,5171
3
- fhir_pyrate/dicom_downloader.py,sha256=wGPMhyY94hdvxCQN1ew1xMsR_UccdnHaNBVixlJuSSM,26196
4
- fhir_pyrate/miner.py,sha256=sCiRhvBRUB548g2M9PcV8AptODdTQzd_KXJ8YccZKNU,7327
5
- fhir_pyrate/pirate.py,sha256=bgim3eWDZtRElFxekS3N0_4LgZ7kFpFYNmXG34oxMls,69421
6
- fhir_pyrate/util/__init__.py,sha256=YapXW-zC11qNTIHJi9IXgLMJmMsVbT3FR_laB-6CxYE,188
7
- fhir_pyrate/util/bundle_processing_templates.py,sha256=rxeUCRKiMMblT_-c55kYm1JofdAAbmDgu5spfBy99uE,4448
8
- fhir_pyrate/util/fhirobj.py,sha256=GX6iwbXtBYpe_DiRag0fYF3qenaLD2bQh31WYPDke44,883
9
- fhir_pyrate/util/imports.py,sha256=jKxiMYTDjbmstqbGCGYjr6EAdbTsvOrZ7GSZo1W6y2g,4336
10
- fhir_pyrate/util/token_auth.py,sha256=Ay6C1mmgotppZZt9RSkEZtWzVxWCdx1QUBMdMQO7cwY,7818
11
- fhir_pyrate/util/util.py,sha256=tE9T9F6WUdhTlQxpWnx-j4P5TqdQr3rimqpFyrlhrGw,1431
12
- fhir_pyrate-0.2.0b9.dist-info/LICENSE,sha256=1IS7y51_JRtpCVVbUpYyztTMj3A6FskEZJBQkAQ9w9o,1084
13
- fhir_pyrate-0.2.0b9.dist-info/METADATA,sha256=YliJr4z4I4YyQEeXyltkQ5nVE2O1uK_aeuga7nmj3zk,26670
14
- fhir_pyrate-0.2.0b9.dist-info/WHEEL,sha256=kLuE8m1WYU0Ig0_YEGrXyTtiJvKPpLpDEiChiNyei5Y,88
15
- fhir_pyrate-0.2.0b9.dist-info/RECORD,,