dic-helper-toolkit 1.0.2__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.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: dic-helper-toolkit
3
+ Version: 1.0.2
4
+ Summary: Helper libraries for data integration centers' daily tasks
5
+ Project-URL: Repository, https://github.com/haemka/dic-helper-toolkit
6
+ Project-URL: Issues, https://github.com/haemka/dic-helper-toolkit/issues
7
+ Requires-Python: >=3.13
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: requests~=2.34.2
10
+ Requires-Dist: requests-oauthlib~=2.0.0
11
+ Requires-Dist: oauthlib~=3.3.1
12
+ Requires-Dist: fhir.resources~=8.2.0
13
+ Requires-Dist: pytest~=9.0.3
14
+ Requires-Dist: certifi~=2026.5.20
15
+ Requires-Dist: charset-normalizer~=3.4.7
16
+ Requires-Dist: pydantic~=2.13.4
17
+ Requires-Dist: typing-extensions~=4.15.0
18
+ Requires-Dist: typing-inspection~=0.4.2
19
+ Requires-Dist: responses~=0.26.1
20
+ Provides-Extra: xml
21
+ Requires-Dist: lxml~=6.1; extra == "xml"
22
+ Provides-Extra: tests
23
+ Requires-Dist: pytest; extra == "tests"
24
+ Requires-Dist: responses; extra == "tests"
25
+ Requires-Dist: lxml~=6.1; extra == "tests"
26
+
27
+ # Python DIC Helper Toolkit
28
+
29
+ This Toolkit contains helper libraries for the data integration centers daily tasks.
30
+
31
+ ## Installation
32
+
33
+ ```shell
34
+ pip install dic-helper-toolkit
35
+ ```
36
+
37
+ The package is published on [PyPI](https://pypi.org/project/dic-helper-toolkit/). To use the FHIR XML reader, install
38
+ the `xml` extra: `pip install dic-helper-toolkit[xml]`.
39
+
40
+ ## Contents
41
+
42
+ ### HTTP client
43
+
44
+ A wrapper around the `requests` library which allows to create an object for an endpoint holding all needed
45
+ configuration including authentication and certificate verification. It supports Basic Auth and OAuth 2.0.
46
+
47
+ #### Usage:
48
+
49
+ ```python
50
+ from dic_helpers.http_client import HTTPClient
51
+
52
+ ## No authentication or CA certificate at all
53
+ client = HTTPClient(base_url="https://example.com")
54
+
55
+ ## Basic Auth with CA certificate
56
+ #client = HTTPClient(base_url="https://example.com", username="user", password="P4SSW0RD", ca_file="MyCA.crt")
57
+
58
+ ## OAuth 2.0 with username and password
59
+ #client = HTTPClient(base_url="https://example.com", username="user", password="P4SSW0RD",
60
+ # auth_url="https://keycloak.example.com", realm="MyRealm", client_id="MyClient",
61
+ # client_secret="00000000-00000000-00000000-00000000", ca_file="MyCA.crt")
62
+
63
+ with client.get("index.html") as result:
64
+ print(result)
65
+ ```
66
+
67
+
68
+ ### FHIR client
69
+
70
+ A simple FHIR client library which uses the HTTP client from above and handles resources directly as `fhir.resources`
71
+ (https://github.com/nazrulworld/fhir.resources) object.
72
+
73
+ #### Usage:
74
+
75
+ ```python
76
+ from dic_helpers.fhir_client import FHIRClient
77
+
78
+ client = FHIRClient(base_url="https://example.com", username="user", password="P4SSW0RD",
79
+ auth_url="https://keycloak.example.com", realm="MyRealm", client_id="MyClient",
80
+ client_secret="00000000-00000000-00000000-00000000", ca_file="MyCA.crt")
81
+
82
+ # Fetch a patient
83
+ patients = client.search("Patient", "identifier=0123456789&_lastUpdated=ge2025-01-01"):
84
+ for patient in patients:
85
+ print(patient)
86
+ ```
87
+ *Keep in mind, that this returns an object.*
88
+
89
+ ### Directory reader
90
+
91
+ Lists the files of a directory, optionally including all subdirectories, whose names match shell-style patterns.
92
+
93
+ #### Usage:
94
+
95
+ ```python
96
+ from dic_helpers.directory_reader import DirectoryReader
97
+
98
+ reader = DirectoryReader("/data/export", patterns=["*.json", "*.ndjson"], recursive=True)
99
+ for path in reader.list_files():
100
+ print(path)
101
+ ```
102
+
103
+ - **Only regular files** are listed, sorted by path, as `pathlib.Path` objects that start with the directory as given.
104
+ - **`patterns`** (default `"*"`) are matched case-sensitively against the file names (`*`, `?`, `[seq]`). A single
105
+ pattern can be a string; a file is listed if it matches any pattern. Special characters in the directory's own name
106
+ don't matter.
107
+ - **`recursive`** (default `False`) also lists the files of all subdirectories.
108
+ - **`include_hidden`** (default `False`): hidden files (names starting with `.`) are skipped and hidden directories
109
+ are not read, unless this is `True`.
110
+ - **`follow_symlinks`** (default `False`): symbolic links to directories are only read if this is `True`. Every
111
+ directory is read once at most, so symlink loops end. Symbolic links to files are always listed, broken links never.
112
+ - **Errors:** a missing directory raises `FileNotFoundError`, a file instead of a directory `NotADirectoryError`, and
113
+ an unreadable directory `PermissionError`. An empty list therefore always means that no file matched.
114
+
115
+
116
+ ### FHIR file reader
117
+
118
+ Reads FHIR R4B resources from files as `fhir.resources` objects, together with the file and line they were read from.
119
+ Combined with the directory reader:
120
+
121
+ ```python
122
+ from dic_helpers.directory_reader import DirectoryReader
123
+ from dic_helpers.fhir_file_reader import FHIRFileReader
124
+
125
+ paths = DirectoryReader("/data/export", patterns=["*.json", "*.ndjson", "*.xml", "*.gz"], recursive=True).list_files()
126
+ for item in FHIRFileReader(paths).resources():
127
+ print(item.path, item.line, item.resource.get_resource_type(), item.resource.id)
128
+ ```
129
+
130
+ **Formats**, by file extension (case-insensitive):
131
+
132
+ | Extension | Content | `item.line` |
133
+ |---|---|---|
134
+ | `.json` | one resource | `None` |
135
+ | `.ndjson` | one resource per line (FHIR bulk data), empty lines are skipped | line number, from 1 |
136
+ | `.xml` | one resource in FHIR XML, the resource type comes from the root element | `None` |
137
+ | `.json.gz`, `.ndjson.gz`, `.xml.gz` | the same, gzip compressed | as above |
138
+
139
+ Other extensions are an error, the format is not guessed. Text is UTF-8, a byte order mark is allowed. Reading XML
140
+ needs `lxml`: install `dic-helper-toolkit[xml]`. External entities are not resolved, an XML file that references one is
141
+ invalid.
142
+
143
+ **Parameters:**
144
+ - **`paths`**: the files, read in this order; a single path can be given on its own. Resources come in the order they
145
+ appear in each file. NDJSON files are read line by line, also compressed ones; JSON and XML files as a whole.
146
+ - **`unpack_bundles`** (default `True`): a Bundle gives the resources of its entries, of any Bundle type. Only the
147
+ outermost Bundle is unpacked, entries without a resource are left out. With `False` the Bundle is returned.
148
+ - **`on_error`** (default `"raise"`): an unreadable or invalid file, NDJSON line or resource raises `FHIRFileError`,
149
+ whose message starts with the file and line, e.g. `export/Patient.ndjson:17: invalid JSON: …` or
150
+ `patient.json: invalid Patient: birthDate: Date value string does not match spec regex.`. With `"skip"` the message
151
+ is logged as a warning, and reading continues with the next line (NDJSON) or the next file. A missing `lxml` is
152
+ always an error.
@@ -0,0 +1,126 @@
1
+ # Python DIC Helper Toolkit
2
+
3
+ This Toolkit contains helper libraries for the data integration centers daily tasks.
4
+
5
+ ## Installation
6
+
7
+ ```shell
8
+ pip install dic-helper-toolkit
9
+ ```
10
+
11
+ The package is published on [PyPI](https://pypi.org/project/dic-helper-toolkit/). To use the FHIR XML reader, install
12
+ the `xml` extra: `pip install dic-helper-toolkit[xml]`.
13
+
14
+ ## Contents
15
+
16
+ ### HTTP client
17
+
18
+ A wrapper around the `requests` library which allows to create an object for an endpoint holding all needed
19
+ configuration including authentication and certificate verification. It supports Basic Auth and OAuth 2.0.
20
+
21
+ #### Usage:
22
+
23
+ ```python
24
+ from dic_helpers.http_client import HTTPClient
25
+
26
+ ## No authentication or CA certificate at all
27
+ client = HTTPClient(base_url="https://example.com")
28
+
29
+ ## Basic Auth with CA certificate
30
+ #client = HTTPClient(base_url="https://example.com", username="user", password="P4SSW0RD", ca_file="MyCA.crt")
31
+
32
+ ## OAuth 2.0 with username and password
33
+ #client = HTTPClient(base_url="https://example.com", username="user", password="P4SSW0RD",
34
+ # auth_url="https://keycloak.example.com", realm="MyRealm", client_id="MyClient",
35
+ # client_secret="00000000-00000000-00000000-00000000", ca_file="MyCA.crt")
36
+
37
+ with client.get("index.html") as result:
38
+ print(result)
39
+ ```
40
+
41
+
42
+ ### FHIR client
43
+
44
+ A simple FHIR client library which uses the HTTP client from above and handles resources directly as `fhir.resources`
45
+ (https://github.com/nazrulworld/fhir.resources) object.
46
+
47
+ #### Usage:
48
+
49
+ ```python
50
+ from dic_helpers.fhir_client import FHIRClient
51
+
52
+ client = FHIRClient(base_url="https://example.com", username="user", password="P4SSW0RD",
53
+ auth_url="https://keycloak.example.com", realm="MyRealm", client_id="MyClient",
54
+ client_secret="00000000-00000000-00000000-00000000", ca_file="MyCA.crt")
55
+
56
+ # Fetch a patient
57
+ patients = client.search("Patient", "identifier=0123456789&_lastUpdated=ge2025-01-01"):
58
+ for patient in patients:
59
+ print(patient)
60
+ ```
61
+ *Keep in mind, that this returns an object.*
62
+
63
+ ### Directory reader
64
+
65
+ Lists the files of a directory, optionally including all subdirectories, whose names match shell-style patterns.
66
+
67
+ #### Usage:
68
+
69
+ ```python
70
+ from dic_helpers.directory_reader import DirectoryReader
71
+
72
+ reader = DirectoryReader("/data/export", patterns=["*.json", "*.ndjson"], recursive=True)
73
+ for path in reader.list_files():
74
+ print(path)
75
+ ```
76
+
77
+ - **Only regular files** are listed, sorted by path, as `pathlib.Path` objects that start with the directory as given.
78
+ - **`patterns`** (default `"*"`) are matched case-sensitively against the file names (`*`, `?`, `[seq]`). A single
79
+ pattern can be a string; a file is listed if it matches any pattern. Special characters in the directory's own name
80
+ don't matter.
81
+ - **`recursive`** (default `False`) also lists the files of all subdirectories.
82
+ - **`include_hidden`** (default `False`): hidden files (names starting with `.`) are skipped and hidden directories
83
+ are not read, unless this is `True`.
84
+ - **`follow_symlinks`** (default `False`): symbolic links to directories are only read if this is `True`. Every
85
+ directory is read once at most, so symlink loops end. Symbolic links to files are always listed, broken links never.
86
+ - **Errors:** a missing directory raises `FileNotFoundError`, a file instead of a directory `NotADirectoryError`, and
87
+ an unreadable directory `PermissionError`. An empty list therefore always means that no file matched.
88
+
89
+
90
+ ### FHIR file reader
91
+
92
+ Reads FHIR R4B resources from files as `fhir.resources` objects, together with the file and line they were read from.
93
+ Combined with the directory reader:
94
+
95
+ ```python
96
+ from dic_helpers.directory_reader import DirectoryReader
97
+ from dic_helpers.fhir_file_reader import FHIRFileReader
98
+
99
+ paths = DirectoryReader("/data/export", patterns=["*.json", "*.ndjson", "*.xml", "*.gz"], recursive=True).list_files()
100
+ for item in FHIRFileReader(paths).resources():
101
+ print(item.path, item.line, item.resource.get_resource_type(), item.resource.id)
102
+ ```
103
+
104
+ **Formats**, by file extension (case-insensitive):
105
+
106
+ | Extension | Content | `item.line` |
107
+ |---|---|---|
108
+ | `.json` | one resource | `None` |
109
+ | `.ndjson` | one resource per line (FHIR bulk data), empty lines are skipped | line number, from 1 |
110
+ | `.xml` | one resource in FHIR XML, the resource type comes from the root element | `None` |
111
+ | `.json.gz`, `.ndjson.gz`, `.xml.gz` | the same, gzip compressed | as above |
112
+
113
+ Other extensions are an error, the format is not guessed. Text is UTF-8, a byte order mark is allowed. Reading XML
114
+ needs `lxml`: install `dic-helper-toolkit[xml]`. External entities are not resolved, an XML file that references one is
115
+ invalid.
116
+
117
+ **Parameters:**
118
+ - **`paths`**: the files, read in this order; a single path can be given on its own. Resources come in the order they
119
+ appear in each file. NDJSON files are read line by line, also compressed ones; JSON and XML files as a whole.
120
+ - **`unpack_bundles`** (default `True`): a Bundle gives the resources of its entries, of any Bundle type. Only the
121
+ outermost Bundle is unpacked, entries without a resource are left out. With `False` the Bundle is returned.
122
+ - **`on_error`** (default `"raise"`): an unreadable or invalid file, NDJSON line or resource raises `FHIRFileError`,
123
+ whose message starts with the file and line, e.g. `export/Patient.ndjson:17: invalid JSON: …` or
124
+ `patient.json: invalid Patient: birthDate: Date value string does not match spec regex.`. With `"skip"` the message
125
+ is logged as a warning, and reading continues with the next line (NDJSON) or the next file. A missing `lxml` is
126
+ always an error.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: dic-helper-toolkit
3
+ Version: 1.0.2
4
+ Summary: Helper libraries for data integration centers' daily tasks
5
+ Project-URL: Repository, https://github.com/haemka/dic-helper-toolkit
6
+ Project-URL: Issues, https://github.com/haemka/dic-helper-toolkit/issues
7
+ Requires-Python: >=3.13
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: requests~=2.34.2
10
+ Requires-Dist: requests-oauthlib~=2.0.0
11
+ Requires-Dist: oauthlib~=3.3.1
12
+ Requires-Dist: fhir.resources~=8.2.0
13
+ Requires-Dist: pytest~=9.0.3
14
+ Requires-Dist: certifi~=2026.5.20
15
+ Requires-Dist: charset-normalizer~=3.4.7
16
+ Requires-Dist: pydantic~=2.13.4
17
+ Requires-Dist: typing-extensions~=4.15.0
18
+ Requires-Dist: typing-inspection~=0.4.2
19
+ Requires-Dist: responses~=0.26.1
20
+ Provides-Extra: xml
21
+ Requires-Dist: lxml~=6.1; extra == "xml"
22
+ Provides-Extra: tests
23
+ Requires-Dist: pytest; extra == "tests"
24
+ Requires-Dist: responses; extra == "tests"
25
+ Requires-Dist: lxml~=6.1; extra == "tests"
26
+
27
+ # Python DIC Helper Toolkit
28
+
29
+ This Toolkit contains helper libraries for the data integration centers daily tasks.
30
+
31
+ ## Installation
32
+
33
+ ```shell
34
+ pip install dic-helper-toolkit
35
+ ```
36
+
37
+ The package is published on [PyPI](https://pypi.org/project/dic-helper-toolkit/). To use the FHIR XML reader, install
38
+ the `xml` extra: `pip install dic-helper-toolkit[xml]`.
39
+
40
+ ## Contents
41
+
42
+ ### HTTP client
43
+
44
+ A wrapper around the `requests` library which allows to create an object for an endpoint holding all needed
45
+ configuration including authentication and certificate verification. It supports Basic Auth and OAuth 2.0.
46
+
47
+ #### Usage:
48
+
49
+ ```python
50
+ from dic_helpers.http_client import HTTPClient
51
+
52
+ ## No authentication or CA certificate at all
53
+ client = HTTPClient(base_url="https://example.com")
54
+
55
+ ## Basic Auth with CA certificate
56
+ #client = HTTPClient(base_url="https://example.com", username="user", password="P4SSW0RD", ca_file="MyCA.crt")
57
+
58
+ ## OAuth 2.0 with username and password
59
+ #client = HTTPClient(base_url="https://example.com", username="user", password="P4SSW0RD",
60
+ # auth_url="https://keycloak.example.com", realm="MyRealm", client_id="MyClient",
61
+ # client_secret="00000000-00000000-00000000-00000000", ca_file="MyCA.crt")
62
+
63
+ with client.get("index.html") as result:
64
+ print(result)
65
+ ```
66
+
67
+
68
+ ### FHIR client
69
+
70
+ A simple FHIR client library which uses the HTTP client from above and handles resources directly as `fhir.resources`
71
+ (https://github.com/nazrulworld/fhir.resources) object.
72
+
73
+ #### Usage:
74
+
75
+ ```python
76
+ from dic_helpers.fhir_client import FHIRClient
77
+
78
+ client = FHIRClient(base_url="https://example.com", username="user", password="P4SSW0RD",
79
+ auth_url="https://keycloak.example.com", realm="MyRealm", client_id="MyClient",
80
+ client_secret="00000000-00000000-00000000-00000000", ca_file="MyCA.crt")
81
+
82
+ # Fetch a patient
83
+ patients = client.search("Patient", "identifier=0123456789&_lastUpdated=ge2025-01-01"):
84
+ for patient in patients:
85
+ print(patient)
86
+ ```
87
+ *Keep in mind, that this returns an object.*
88
+
89
+ ### Directory reader
90
+
91
+ Lists the files of a directory, optionally including all subdirectories, whose names match shell-style patterns.
92
+
93
+ #### Usage:
94
+
95
+ ```python
96
+ from dic_helpers.directory_reader import DirectoryReader
97
+
98
+ reader = DirectoryReader("/data/export", patterns=["*.json", "*.ndjson"], recursive=True)
99
+ for path in reader.list_files():
100
+ print(path)
101
+ ```
102
+
103
+ - **Only regular files** are listed, sorted by path, as `pathlib.Path` objects that start with the directory as given.
104
+ - **`patterns`** (default `"*"`) are matched case-sensitively against the file names (`*`, `?`, `[seq]`). A single
105
+ pattern can be a string; a file is listed if it matches any pattern. Special characters in the directory's own name
106
+ don't matter.
107
+ - **`recursive`** (default `False`) also lists the files of all subdirectories.
108
+ - **`include_hidden`** (default `False`): hidden files (names starting with `.`) are skipped and hidden directories
109
+ are not read, unless this is `True`.
110
+ - **`follow_symlinks`** (default `False`): symbolic links to directories are only read if this is `True`. Every
111
+ directory is read once at most, so symlink loops end. Symbolic links to files are always listed, broken links never.
112
+ - **Errors:** a missing directory raises `FileNotFoundError`, a file instead of a directory `NotADirectoryError`, and
113
+ an unreadable directory `PermissionError`. An empty list therefore always means that no file matched.
114
+
115
+
116
+ ### FHIR file reader
117
+
118
+ Reads FHIR R4B resources from files as `fhir.resources` objects, together with the file and line they were read from.
119
+ Combined with the directory reader:
120
+
121
+ ```python
122
+ from dic_helpers.directory_reader import DirectoryReader
123
+ from dic_helpers.fhir_file_reader import FHIRFileReader
124
+
125
+ paths = DirectoryReader("/data/export", patterns=["*.json", "*.ndjson", "*.xml", "*.gz"], recursive=True).list_files()
126
+ for item in FHIRFileReader(paths).resources():
127
+ print(item.path, item.line, item.resource.get_resource_type(), item.resource.id)
128
+ ```
129
+
130
+ **Formats**, by file extension (case-insensitive):
131
+
132
+ | Extension | Content | `item.line` |
133
+ |---|---|---|
134
+ | `.json` | one resource | `None` |
135
+ | `.ndjson` | one resource per line (FHIR bulk data), empty lines are skipped | line number, from 1 |
136
+ | `.xml` | one resource in FHIR XML, the resource type comes from the root element | `None` |
137
+ | `.json.gz`, `.ndjson.gz`, `.xml.gz` | the same, gzip compressed | as above |
138
+
139
+ Other extensions are an error, the format is not guessed. Text is UTF-8, a byte order mark is allowed. Reading XML
140
+ needs `lxml`: install `dic-helper-toolkit[xml]`. External entities are not resolved, an XML file that references one is
141
+ invalid.
142
+
143
+ **Parameters:**
144
+ - **`paths`**: the files, read in this order; a single path can be given on its own. Resources come in the order they
145
+ appear in each file. NDJSON files are read line by line, also compressed ones; JSON and XML files as a whole.
146
+ - **`unpack_bundles`** (default `True`): a Bundle gives the resources of its entries, of any Bundle type. Only the
147
+ outermost Bundle is unpacked, entries without a resource are left out. With `False` the Bundle is returned.
148
+ - **`on_error`** (default `"raise"`): an unreadable or invalid file, NDJSON line or resource raises `FHIRFileError`,
149
+ whose message starts with the file and line, e.g. `export/Patient.ndjson:17: invalid JSON: …` or
150
+ `patient.json: invalid Patient: birthDate: Date value string does not match spec regex.`. With `"skip"` the message
151
+ is logged as a warning, and reading continues with the next line (NDJSON) or the next file. A missing `lxml` is
152
+ always an error.
@@ -0,0 +1,16 @@
1
+ README.md
2
+ pyproject.toml
3
+ dic_helper_toolkit.egg-info/PKG-INFO
4
+ dic_helper_toolkit.egg-info/SOURCES.txt
5
+ dic_helper_toolkit.egg-info/dependency_links.txt
6
+ dic_helper_toolkit.egg-info/requires.txt
7
+ dic_helper_toolkit.egg-info/top_level.txt
8
+ dic_helpers/__init__.py
9
+ dic_helpers/directory_reader.py
10
+ dic_helpers/fhir_client.py
11
+ dic_helpers/fhir_file_reader.py
12
+ dic_helpers/http_client.py
13
+ tests/test_directory_reader.py
14
+ tests/test_fhir_client.py
15
+ tests/test_fhir_file_reader.py
16
+ tests/test_http_client.py
@@ -0,0 +1,19 @@
1
+ requests~=2.34.2
2
+ requests-oauthlib~=2.0.0
3
+ oauthlib~=3.3.1
4
+ fhir.resources~=8.2.0
5
+ pytest~=9.0.3
6
+ certifi~=2026.5.20
7
+ charset-normalizer~=3.4.7
8
+ pydantic~=2.13.4
9
+ typing-extensions~=4.15.0
10
+ typing-inspection~=0.4.2
11
+ responses~=0.26.1
12
+
13
+ [tests]
14
+ pytest
15
+ responses
16
+ lxml~=6.1
17
+
18
+ [xml]
19
+ lxml~=6.1
File without changes
@@ -0,0 +1,80 @@
1
+ """Listing the files of a directory, optionally including its subdirectories."""
2
+ import fnmatch
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Iterable, List, Set, Tuple, Union
6
+
7
+
8
+ class DirectoryReader:
9
+ """
10
+ Lists the files of a directory whose names match one of the given patterns.
11
+ """
12
+
13
+ def __init__(self, directory: Union[str, os.PathLike], patterns: Union[str, Iterable[str]] = ("*",),
14
+ recursive: bool = False, include_hidden: bool = False, follow_symlinks: bool = False) -> None:
15
+ """
16
+ Initialize the DirectoryReader. The directory is only read by list_files().
17
+
18
+ :param directory: The directory to read.
19
+ :type directory: Union[str, os.PathLike]
20
+ :param patterns: Shell-style patterns (fnmatch: *, ?, [seq]) matched case-sensitively against the file names,
21
+ e.g. ("*.json", "*.ndjson"). A single pattern can be given as a string. A file is listed if it matches any
22
+ pattern.
23
+ :type patterns: Union[str, Iterable[str]]
24
+ :param recursive: Also list the files of all subdirectories.
25
+ :type recursive: bool
26
+ :param include_hidden: Also list hidden files (names starting with '.') and read hidden subdirectories.
27
+ :type include_hidden: bool
28
+ :param follow_symlinks: Also read subdirectories that are symbolic links. Every directory is read once at most,
29
+ so symlink loops end. Symbolic links to files are always listed.
30
+ :type follow_symlinks: bool
31
+ """
32
+ self.directory = Path(directory)
33
+ self.patterns: Tuple[str, ...] = (patterns,) if isinstance(patterns, str) else tuple(patterns)
34
+ self.recursive = recursive
35
+ self.include_hidden = include_hidden
36
+ self.follow_symlinks = follow_symlinks
37
+
38
+ def list_files(self) -> List[Path]:
39
+ """
40
+ List the matching files.
41
+
42
+ :return: The paths of the matching regular files, sorted. Each path starts with the directory as given.
43
+ :rtype: List[Path]
44
+ :raises FileNotFoundError: If the directory doesn't exist.
45
+ :raises NotADirectoryError: If the path is not a directory.
46
+ :raises PermissionError: If the directory or one of the subdirectories to read can't be read.
47
+ """
48
+ if not self.directory.exists():
49
+ raise FileNotFoundError(f"Directory not found: {self.directory}")
50
+ if not self.directory.is_dir():
51
+ raise NotADirectoryError(f"Not a directory: {self.directory}")
52
+ files: List[Path] = []
53
+ self._read(self.directory, files, visited=set())
54
+ return sorted(files)
55
+
56
+ def _read(self, directory: Path, files: List[Path], visited: Set[Tuple[int, int]]) -> None:
57
+ """
58
+ Add the matching files of a directory to files, and read its subdirectories if recursive.
59
+
60
+ :param directory: The directory to read.
61
+ :type directory: Path
62
+ :param files: The list the matching files are added to.
63
+ :type files: List[Path]
64
+ :param visited: Device and inode of the directories read so far; directories in it are not read again.
65
+ :type visited: Set[Tuple[int, int]]
66
+ """
67
+ status = directory.stat()
68
+ visited.add((status.st_dev, status.st_ino))
69
+ with os.scandir(directory) as entries:
70
+ for entry in entries:
71
+ if not self.include_hidden and entry.name.startswith("."):
72
+ continue
73
+ # is_file() follows symbolic links: links to files are listed, broken links are not
74
+ if entry.is_file():
75
+ if any(fnmatch.fnmatchcase(entry.name, pattern) for pattern in self.patterns):
76
+ files.append(Path(entry.path))
77
+ elif self.recursive and entry.is_dir(follow_symlinks=self.follow_symlinks):
78
+ status = entry.stat()
79
+ if (status.st_dev, status.st_ino) not in visited:
80
+ self._read(Path(entry.path), files, visited)