Skip to content

Commit 4277e7b

Browse files
authored
Merge pull request #152 from UMEssen/151-auth-with-a-static-token-not-working
151 auth with a static token not working
2 parents 6becdb0 + 468e89d commit 4277e7b

16 files changed

Lines changed: 1957 additions & 1462 deletions
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ jobs:
2828
python-version: ["3.10", "3.11", "3.12"]
2929

3030
steps:
31-
- uses: actions/checkout@v4
31+
- uses: actions/checkout@v6
3232

3333
- name: Set up Python ${{ matrix.python-version }}
34-
uses: actions/setup-python@v5
34+
uses: actions/setup-python@v6
3535
with:
3636
python-version: ${{ matrix.python-version }}
3737

@@ -41,10 +41,11 @@ jobs:
4141
pip install uv
4242
uv sync --group dev --extra all
4343
44-
- name: Run pre-commit
44+
- if: matrix.python-version == '3.10'
45+
name: Run pre-commit
4546
run: |
4647
uv run pre-commit run -a
4748
4849
- name: Run pytest
4950
run: |
50-
uv run pytest -q
51+
uv run pytest

.pre-commit-config.yaml

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
# See https://gitlab.uk-essen.de/ship-ai/templates/pre-commit-hooks
2-
1+
exclude: ^tests/
32
repos:
4-
- repo: https://github.com/charliermarsh/ruff-pre-commit
5-
rev: v0.14.14
3+
- repo: https://github.com/astral-sh/ruff-pre-commit
4+
rev: v0.15.12
65
hooks:
7-
- id: ruff
6+
- id: ruff-check
87
args: [--fix]
98
exclude: examples
109
- id: ruff-format
1110
exclude: examples
1211

1312
- repo: https://github.com/pre-commit/mirrors-mypy
14-
rev: v1.19.1
13+
rev: v1.20.2
1514
hooks:
1615
- id: mypy
1716
language_version: python3.10
@@ -25,36 +24,31 @@ repos:
2524
exclude: node_modules
2625

2726
- repo: https://github.com/tox-dev/pyproject-fmt
28-
rev: v2.12.0
27+
rev: v2.21.2
2928
hooks:
3029
- id: pyproject-fmt
3130
types: [toml]
3231

3332
- repo: https://github.com/DavidAnson/markdownlint-cli2
34-
rev: v0.20.0
33+
rev: v0.22.1
3534
hooks:
3635
- id: markdownlint-cli2
3736
args: [--fix]
3837

3938
- repo: https://github.com/codespell-project/codespell
40-
rev: v2.4.1
39+
rev: v2.4.2
4140
hooks:
4241
- id: codespell
4342
types: [markdown]
4443

45-
- repo: local
46-
hooks:
47-
- id: export-requirements
48-
name: Export requirements from uv
49-
entry: uv pip compile
50-
args: ["pyproject.toml", "--output-file", "requirements.txt"]
51-
language: system
52-
pass_filenames: false
53-
always_run: true
54-
5544
- repo: https://github.com/pre-commit/pre-commit-hooks
5645
rev: v6.0.0
5746
hooks:
58-
- id: end-of-file-fixer
59-
- id: check-merge-conflict
6047
- id: check-added-large-files
48+
- id: check-json
49+
- id: check-merge-conflict
50+
- id: check-toml
51+
- id: check-yaml
52+
- id: detect-private-key
53+
- id: end-of-file-fixer
54+
- id: trailing-whitespace

README.md

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# FHIR-PYrate
22

33
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4-
[![Supported Python version](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/release/python-31011/)
4+
[![Supported Python version](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org)
55
[![Stable Version](https://img.shields.io/pypi/v/fhir-pyrate?label=stable)](https://pypi.org/project/fhir-pyrate/)
66
[![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)
7-
[![DOI](https://zenodo.org/badge/456893108.svg)](https://zenodo.org/badge/latestdoi/456893108)
7+
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7025226.svg)](https://doi.org/10.5281/zenodo.7025226)
88
[![Affiliated with RTG WisPerMed](https://img.shields.io/badge/Affiliated-RTG%202535%20WisPerMed-blue)](https://wispermed.org/)
99

1010
<!-- PROJECT LOGO -->
@@ -46,7 +46,7 @@ Table of Contents:
4646

4747
* [Install](https://github.com/UMEssen/FHIR-PYrate/#install)
4848
* [Either Pip](https://github.com/UMEssen/FHIR-PYrate/#either-pip)
49-
* [Or Within Poetry](https://github.com/UMEssen/FHIR-PYrate/#or-within-poetry)
49+
* [Or Within uv](https://github.com/UMEssen/FHIR-PYrate/#or-within-uv)
5050
* [Run Tests](https://github.com/UMEssen/FHIR-PYrate/#run-tests)
5151
* [Explanations &amp; Examples](https://github.com/UMEssen/FHIR-PYrate/#explanations--examples)
5252
* [Ahoy](https://github.com/UMEssen/FHIR-PYrate/#ahoy)
@@ -86,49 +86,32 @@ pip install "fhir-pyrate[downloader]" # only for downloader
8686
pip install "fhir-pyrate[all]" # for both
8787
```
8888

89-
### Or Within Poetry
89+
### Or Within uv
9090

91-
We can also use poetry for this same purpose. Using PyPi we need to run the following commands.
91+
We can also use [uv](https://docs.astral.sh/uv/) for this same purpose. To add it from PyPi run:
9292

9393
```bash
94-
poetry add fhir-pyrate
95-
poetry install
94+
uv add fhir-pyrate
9695
```
9796

98-
Whereas to add it from GitHub, we have different options, because until recently
99-
[poetry used to exclusively install from the master branch](https://github.com/python-poetry/poetry/issues/3366).
100-
101-
Poetry 1.2.0a2+:
102-
103-
```bash
104-
poetry add git+https://github.com/UMEssen/FHIR-PYrate.git
105-
poetry install
106-
```
107-
108-
For the previous versions you need to add the following line to your `pyproject.toml` file:
109-
110-
```bash
111-
fhir-pyrate = {git = "https://github.com/UMEssen/FHIR-PYrate.git", branch = "main"}
112-
```
113-
114-
and then run
97+
Whereas to add it from GitHub (always the newest version):
11598

11699
```bash
117-
poetry lock
100+
uv add git+https://github.com/UMEssen/FHIR-PYrate.git
118101
```
119102

120-
Also in poetry, the above only installs the packages for **Pirate**. If you also want to use the **Miner** or the **DicomDownloader**, then you need to install them as extra dependencies with
103+
Also in uv, the above only installs the packages for **Pirate**. If you also want to use the **Miner** or the **DicomDownloader**, then you need to install them as extra dependencies with
121104

122105
```bash
123-
poetry add "fhir-pyrate[miner]" # only for miner
124-
poetry add "fhir-pyrate[downloader]" # only for downloader
125-
poetry add "fhir-pyrate[all]" # for both
106+
uv add "fhir-pyrate[miner]" # only for miner
107+
uv add "fhir-pyrate[downloader]" # only for downloader
108+
uv add "fhir-pyrate[all]" # for both
126109
```
127110

128-
or by adding the following to your `pyproject.toml` file:
111+
The extras can be combined with the GitHub source as well:
129112

130113
```bash
131-
fhir-pyrate = {git = "https://github.com/UMEssen/FHIR-PYrate.git", branch = "main", extras = ["all"]}
114+
uv add "fhir-pyrate[all] @ git+https://github.com/UMEssen/FHIR-PYrate.git"
132115
```
133116

134117
## Run Tests
@@ -138,7 +121,7 @@ unit tests. First set the `FHIR_USER` and `FHIR_PASSWORD` environment variables
138121
username and password for the FHIR server and then run the tests.
139122

140123
```bash
141-
poetry run python -m unittest discover tests
124+
uv run pytest
142125
```
143126

144127
If you implement a new feature, please add a small test for it in
@@ -269,7 +252,7 @@ generated. You can drop the ID duplicates once you have built a DataFrame with y
269252

270253
#### [`trade_rows_for_bundles`](https://github.com/UMEssen/FHIR-PYrate/blob/main/fhir_pyrate/pirate.py)
271254

272-
In case we already have an Excel sheet or CSV file with `fhir_patient_id`s or any other
255+
In case we already have an Excel sheet or CSV file with `fhir_patient_id`s (or any other
273256
identifier), and we want to request resources based on those
274257
identifiers we can use the function `trade_rows_for_bundles`:
275258

@@ -292,7 +275,7 @@ DataFrame in `df_constraints`. This dictionary should contain pairs of (`fhir_id
292275
`identifier_column`) where `fhir_identifier` is the API search parameter and `identifier_column`
293276
is the column where the values that we want to search for are stored.
294277
Additionally, a system can be used to better identify the constraints of the DataFrame.
295-
For example, let us assume that we have a column of the DataFrame (called `loinc_code` that
278+
For example, let us assume that we have a column of the DataFrame called `loinc_code` that
296279
contains a bunch of different LOINC codes. Our `df_constraints` could look as follows:
297280

298281
```python
@@ -522,8 +505,8 @@ your institute, please do not hesitate and contact us, or write a pull request!
522505
The **DicomDownloader** downloads a complete Study (StudyInstanceUID) or a specific series (
523506
StudyInstanceUID + SeriesInstanceUID).
524507

525-
The relevant data can be downloaded either es DICOM (`.dcm`) or NIfTI (`.nii.gz`).
526-
In the NIfTI case there will be an additional `.dcm` file to store some metadata.
508+
The relevant data can be downloaded either as DICOM (`.dcm`) or NIfTI (`.nii.gz`).
509+
In the NIfTI case there will be an additional `.dcm` file to store some metadata.
527510

528511
Using the function `download_data_from_dataframe` it is possible to download studies and series
529512
directly from the data of a given dataframe. The column that contain the study/series

fhir_pyrate/ahoy.py

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import os
44
from datetime import timedelta
55
from types import TracebackType
6-
from typing import Optional, Type, Union
76

87
import requests
98
from requests.auth import HTTPBasicAuth
@@ -27,8 +26,10 @@ class Ahoy:
2726
password will use the given username as username and ask to input a password;
2827
env will use the environment variables FHIR_USER and FHIR_PASSWORD;
2928
keyring will use a keyring [NOT IMPLEMENTED YET]
30-
:param token: The token that can be used for authentication, if this variable is used then
31-
the other variables do not need to be specified
29+
:param token: A pre-existing token to use for authentication. If this is given, no login is
30+
performed and the token is used as-is, so the username/password/auth_method variables do not
31+
need to be specified. An auth_url and/or refresh_url are then only needed if the token should
32+
be refreshed once it expires.
3233
:param max_login_attempts: The maximum number of logins that can be performed
3334
:param token_refresh_delta: Either a timedelta object that tells us how often the token
3435
should be refreshed, or a number of minutes; this does not need to be specified for JWT tokens
@@ -40,31 +41,33 @@ class Ahoy:
4041

4142
def __init__(
4243
self,
43-
auth_url: Optional[str] = None,
44-
auth_type: Optional[str] = "token",
45-
refresh_url: Optional[str] = None,
46-
username: Optional[str] = None,
47-
auth_method: Optional[str] = "password",
48-
token: Optional[str] = None,
44+
auth_url: str | None = None,
45+
auth_type: str | None = "token",
46+
refresh_url: str | None = None,
47+
username: str | None = None,
48+
auth_method: str | None = "password",
49+
token: str | None = None,
4950
max_login_attempts: int = 5,
50-
token_refresh_delta: Optional[Union[int, timedelta]] = None,
51-
session: Optional[requests.Session] = None,
51+
token_refresh_delta: int | timedelta | None = None,
52+
session: requests.Session | None = None,
5253
) -> None:
5354
self.auth_type = auth_type
5455
self.auth_method = auth_method
5556
self.auth_url = auth_url
5657
self.refresh_url = refresh_url
5758
self.username = username
5859
self._user_env_name = "FHIR_USER"
59-
self._pass_env_name = "FHIR_PASSWORD"
60+
self._pass_env_name = "FHIR_PASSWORD" # noqa: S105
6061
self.token = token
6162
if session is None:
6263
self.session = requests.Session()
6364
else:
6465
self.session = session
6566
self.max_login_attempts = max_login_attempts
6667
self.token_refresh_delta = token_refresh_delta
67-
if self.auth_type is not None and self.auth_method is not None:
68+
if self.token is not None or (
69+
self.auth_type is not None and self.auth_method is not None
70+
):
6871
self._authenticate()
6972

7073
def __enter__(self) -> "Ahoy":
@@ -75,14 +78,14 @@ def close(self) -> None:
7578

7679
def __exit__(
7780
self,
78-
exctype: Optional[Type[BaseException]],
79-
excinst: Optional[BaseException],
80-
exctb: Optional[TracebackType],
81+
exctype: type[BaseException] | None,
82+
excinst: BaseException | None,
83+
exctb: TracebackType | None,
8184
) -> None:
8285
self.close()
8386

8487
def change_environment_variable_name(
85-
self, user_env: Optional[str] = None, pass_env: Optional[str] = None
88+
self, user_env: str | None = None, pass_env: str | None = None
8689
) -> None:
8790
"""
8891
Change the name of the variables used to retrieve username and password.
@@ -100,6 +103,22 @@ def _authenticate(self) -> None:
100103
"""
101104
Authenticate the user in the current session with a token or with BasicAuth.
102105
"""
106+
assert self.auth_type is not None
107+
if self.token is not None:
108+
if self.auth_type.lower() != "token":
109+
raise ValueError(
110+
"A pre-existing token can only be used with the 'token' "
111+
f"authentication type, but {self.auth_type} was given."
112+
)
113+
self.session.auth = TokenAuth(
114+
auth_url=self.auth_url,
115+
refresh_url=self.refresh_url,
116+
session=self.session,
117+
max_login_attempts=self.max_login_attempts,
118+
token_refresh_delta=self.token_refresh_delta,
119+
token=self.token,
120+
)
121+
return
103122
assert self.auth_method is not None
104123
if self.auth_method.lower() == "password":
105124
assert self.username is not None, (
@@ -121,7 +140,6 @@ def _authenticate(self) -> None:
121140
raise ValueError(
122141
f"Used authentication method {self.auth_method} is not defined."
123142
)
124-
assert self.auth_type is not None
125143
if self.auth_type.lower() == "token":
126144
assert self.auth_url is not None, (
127145
"The token authentication method cannot be used "

0 commit comments

Comments
 (0)