fundus-camera-watchdog 0.2.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.
@@ -0,0 +1,264 @@
1
+ Metadata-Version: 2.3
2
+ Name: fundus-camera-watchdog
3
+ Version: 0.2.0
4
+ Summary: File watchdog for fundus camera
5
+ Requires-Dist: requests>=2.34.2
6
+ Requires-Dist: tzdata>=2026.2
7
+ Requires-Dist: watchdog>=6.0.0
8
+ Requires-Python: >=3.12, <3.15
9
+ Project-URL: Homepage, https://github.com/erikvw/fundus-camera-watchdog
10
+ Description-Content-Type: text/x-rst
11
+
12
+ |pypi| |actions| |codecov| |downloads| |clinicedc|
13
+
14
+ Fundus Camera Watchdog
15
+ ======================
16
+
17
+ ``camera_watchdog.py`` monitors a folder on the fundus camera workstation and uploads files to a CLINICEDC project using the ``edc-retinopathy`` API.
18
+
19
+ It is designed to run on the camera's workstation. When the camera finishes an examination and writes files to disk, the watchdog detects them, resolves the subject against the CLINICEDC server, uploads each file, and moves the completed folder to an archive folder.
20
+
21
+ Prerequisites
22
+ -------------
23
+
24
+ 1. A running CLINICEDC server with ``edc-retinopathy`` installed and an API token created.
25
+
26
+ 2. The camera software must write its output into the watched folder using the expected layout (see `Folder layout`_ below).
27
+
28
+ 3. The camera's SQLite database must be accessible from the workstation (typically a local file).
29
+
30
+ Folder layout
31
+ -------------
32
+
33
+ The camera creates one subfolder per subject, named with the subject identifier. Inside each subfolder are UUID-named files.
34
+
35
+ **Combined report** (default — one HTML covers both eyes)::
36
+
37
+ C:\RetCamOutput\
38
+ 105-10-0989-3\
39
+ a1b2c3d4.jpg <- eye image (left or right)
40
+ e5f6a7b8.jpg <- eye image (left or right)
41
+ c9d0e1f2.html <- combined report (both eyes)
42
+ 105-10-0001-2\
43
+ ...
44
+
45
+ **Per-eye report** (one HTML per eye)::
46
+
47
+ C:\RetCamOutput\
48
+ 105-10-0989-3\
49
+ a1b2c3d4.jpg <- eye image (left or right)
50
+ e5f6a7b8.jpg <- eye image (left or right)
51
+ c9d0e1f2.html <- eye report (left or right)
52
+ f3a4b5c6.html <- eye report (left or right)
53
+ 105-10-0001-2\
54
+ ...
55
+
56
+ Because filenames are random UUIDs, the watchdog queries the camera's SQLite database to determine which file belongs to which eye.
57
+
58
+ Processing is triggered once a subject folder contains the expected number
59
+ of files:
60
+
61
+ - **Combined** (default): at least **2 JPEG** and **1 HTML** file (3 files total).
62
+ - **Per-eye**: at least **2 JPEG** and **2 HTML** files (4 files total).
63
+
64
+ Configuration
65
+ -------------
66
+
67
+ All settings live in a single JSON file. Create ``camera_config.json``::
68
+
69
+ {
70
+ "watch_dir": "C:\\RetCamOutput",
71
+ "db_path": "C:\\RetCamOutput\\camera.db",
72
+ "api_url": "https://edc.example.com",
73
+ "token": "YOUR_DRF_TOKEN",
74
+ "device_id": "RET-CAM-001",
75
+ "site_id": "40",
76
+
77
+ "db_patient_table": "patients",
78
+ "db_patient_subject_id": "subject_identifier",
79
+ "db_patient_initials": "initials",
80
+ "db_patient_sex": "sex",
81
+ "db_patient_age": "age",
82
+
83
+ "db_image_table": "images",
84
+ "db_image_subject_id": "subject_identifier",
85
+ "db_image_filename": "filename",
86
+ "db_image_eye": "eye",
87
+
88
+ "report_type": "combined"
89
+ }
90
+
91
+ Required keys
92
+ ~~~~~~~~~~~~~
93
+
94
+ ``watch_dir``
95
+ Folder the camera writes subject subfolders to.
96
+
97
+ ``db_path``
98
+ Path to the camera's SQLite database.
99
+
100
+ ``api_url``
101
+ Base URL of the EDC server (e.g. ``https://edc.example.com``).
102
+
103
+ ``token``
104
+ DRF authentication token for the camera user.
105
+
106
+ Optional keys
107
+ ~~~~~~~~~~~~~
108
+
109
+ ``device_id``
110
+ Identifier for this camera (sent to the server with each session).
111
+
112
+ ``site_id``
113
+ Study site identifier.
114
+
115
+ ``report_type``
116
+ How the camera writes its analysis reports. ``combined`` (default) means
117
+ a single HTML file covers both eyes; ``per_eye`` means one HTML per eye.
118
+ This controls how many files the watchdog expects before triggering an
119
+ upload (3 for combined, 4 for per_eye).
120
+
121
+ ``log_level``
122
+ One of ``DEBUG``, ``INFO`` (default), ``WARNING``, ``ERROR``.
123
+
124
+ Database column mapping
125
+ ~~~~~~~~~~~~~~~~~~~~~~~
126
+
127
+ These keys tell the watchdog which tables and columns to query in the
128
+ camera's SQLite database. Adjust them to match your camera vendor's
129
+ actual schema.
130
+
131
+ +-----------------------------+------------------------------------------+--------------------------+
132
+ | Config key | Purpose | Default |
133
+ +=============================+==========================================+==========================+
134
+ | ``db_patient_table`` | Table containing patient demographics | ``patients`` |
135
+ +-----------------------------+------------------------------------------+--------------------------+
136
+ | ``db_patient_subject_id`` | Column for subject identifier | ``subject_identifier`` |
137
+ +-----------------------------+------------------------------------------+--------------------------+
138
+ | ``db_patient_initials`` | Column for initials | ``initials`` |
139
+ +-----------------------------+------------------------------------------+--------------------------+
140
+ | ``db_patient_sex`` | Column for sex (M/F) | ``sex`` |
141
+ +-----------------------------+------------------------------------------+--------------------------+
142
+ | ``db_patient_age`` | Column for age in years | ``age`` |
143
+ +-----------------------------+------------------------------------------+--------------------------+
144
+ | ``db_image_table`` | Table mapping files to eyes | ``images`` |
145
+ +-----------------------------+------------------------------------------+--------------------------+
146
+ | ``db_image_subject_id`` | Column for subject identifier | ``subject_identifier`` |
147
+ +-----------------------------+------------------------------------------+--------------------------+
148
+ | ``db_image_filename`` | Column for UUID filename | ``filename`` |
149
+ +-----------------------------+------------------------------------------+--------------------------+
150
+ | ``db_image_eye`` | Column for eye laterality | ``eye`` |
151
+ +-----------------------------+------------------------------------------+--------------------------+
152
+
153
+ The watchdog normalises eye values automatically. All of the following
154
+ are recognised:
155
+
156
+ - **Left eye**: ``L``, ``LE``, ``OS``, ``LEFT``
157
+ - **Right eye**: ``R``, ``RE``, ``OD``, ``RIGHT``
158
+
159
+ Usage
160
+ -----
161
+
162
+ With a config file (recommended)::
163
+
164
+ uv run camera_watchdog.py --config camera_config.json
165
+
166
+ CLI flags override any value from the config file::
167
+
168
+ uv run camera_watchdog.py --config camera_config.json --log-level DEBUG --report-type per_eye
169
+
170
+ Without a config file (all flags on the command line)::
171
+
172
+ uv run camera_watchdog.py ^
173
+ --watch-dir C:\RetCamOutput ^
174
+ --db-path C:\RetCamOutput\camera.db ^
175
+ --api-url https://edc.example.com ^
176
+ --token YOUR_TOKEN ^
177
+ --device-id RET-CAM-001 ^
178
+ --db-patient-table Exams ^
179
+ --db-patient-subject-id patient_code ^
180
+ --db-image-table CapturedFiles ^
181
+ --db-image-eye laterality
182
+
183
+ Stop with ``Ctrl+C``.
184
+
185
+ What it does
186
+ ------------
187
+
188
+ The watchdog runs continuously and performs the following for each
189
+ subject:
190
+
191
+ 1. **Detect** — watches for new files in subject subfolders using
192
+ filesystem events (``watchdog``), plus a periodic sweep every 60 seconds as a safety net.
193
+
194
+ 2. **Wait** — each file is given up to 30 seconds to stabilise (stop growing) before being registered, so half-written files from the camera are not picked up prematurely.
195
+
196
+ 3. **Query camera DB** — retrieves the subject's demographics
197
+ (initials, sex, age) and determines which JPEG is the left eye
198
+ and which is the right. In per-eye mode, the HTML reports are also
199
+ mapped to eyes; in combined mode the HTML is uploaded without eye
200
+ mapping.
201
+
202
+ 4. **Resolve** — ``POST /api/retinopathy/resolve/`` validates the
203
+ subject against ``RegisteredSubject`` on the CLINICEDC server and
204
+ creates (or reactivates) a session.
205
+
206
+ 5. **Upload** — sends each file to the correct API endpoint:
207
+
208
+ - ``*.jpg`` + left eye -> ``POST .../left/``
209
+ - ``*.jpg`` + right eye -> ``POST .../right/``
210
+ - ``*.html`` (combined) -> ``POST .../report/``
211
+ - ``*.html`` + left eye (per_eye) -> ``POST .../left_report/``
212
+ - ``*.html`` + right eye (per_eye) -> ``POST .../right_report/``
213
+
214
+ Each upload includes a SHA-256 checksum for integrity verification.
215
+
216
+ 6. **Verify** — ``GET .../status/`` confirms the session received all expected files.
217
+
218
+ 7. **Archive** — the entire subject folder is moved to
219
+ ``<watch-dir>/processed/<subject_id>_<timestamp>/``.
220
+
221
+ Error handling
222
+ --------------
223
+
224
+ - **Retries** — every API call is retried up to 3 times with a
225
+ 5-second delay between attempts.
226
+
227
+ - **Failed subjects** — if any step fails (server unreachable,
228
+ validation error, upload failure), the subject is marked as
229
+ unprocessed and will be retried on the next 60-second sweep.
230
+
231
+ - **Startup scan** — on (re)start the watchdog scans all existing
232
+ subject folders, so a restart after a crash picks up where it left off.
233
+
234
+ - **Thread safety** — file detection and upload run on separate
235
+ threads with proper locking, so multiple subjects can be uploaded concurrently.
236
+
237
+ Inspecting the camera database
238
+ ------------------------------
239
+
240
+ To discover your camera's actual table and column names, open the
241
+ SQLite database and list its schema::
242
+
243
+ sqlite3 camera.db
244
+ .tables
245
+ .schema patients
246
+ .schema images
247
+
248
+ Then update the ``db_*`` keys in your config file to match.
249
+
250
+ .. |pypi| image:: https://img.shields.io/pypi/v/fundus-camera-watchdog.svg
251
+ :target: https://pypi.python.org/pypi/fundus-camera-watchdog
252
+
253
+ .. |actions| image:: https://github.com/erikvw/fundus-camera-watchdog/actions/workflows/build.yml/badge.svg
254
+ :target: https://github.com/erikvw/fundus-camera-watchdog/actions/workflows/build.yml
255
+
256
+ .. |codecov| image:: https://codecov.io/gh/erikvw/fundus-camera-watchdog/branch/develop/graph/badge.svg
257
+ :target: https://codecov.io/gh/erikvw/fundus-camera-watchdog
258
+
259
+ .. |downloads| image:: https://pepy.tech/badge/fundus-camera-watchdog
260
+ :target: https://pepy.tech/project/fundus-camera-watchdog
261
+
262
+ .. |clinicedc| image:: https://img.shields.io/badge/framework-Clinic_EDC-green
263
+ :alt:Made with clinicedc
264
+ :target: https://github.com/clinicedc
@@ -0,0 +1,253 @@
1
+ |pypi| |actions| |codecov| |downloads| |clinicedc|
2
+
3
+ Fundus Camera Watchdog
4
+ ======================
5
+
6
+ ``camera_watchdog.py`` monitors a folder on the fundus camera workstation and uploads files to a CLINICEDC project using the ``edc-retinopathy`` API.
7
+
8
+ It is designed to run on the camera's workstation. When the camera finishes an examination and writes files to disk, the watchdog detects them, resolves the subject against the CLINICEDC server, uploads each file, and moves the completed folder to an archive folder.
9
+
10
+ Prerequisites
11
+ -------------
12
+
13
+ 1. A running CLINICEDC server with ``edc-retinopathy`` installed and an API token created.
14
+
15
+ 2. The camera software must write its output into the watched folder using the expected layout (see `Folder layout`_ below).
16
+
17
+ 3. The camera's SQLite database must be accessible from the workstation (typically a local file).
18
+
19
+ Folder layout
20
+ -------------
21
+
22
+ The camera creates one subfolder per subject, named with the subject identifier. Inside each subfolder are UUID-named files.
23
+
24
+ **Combined report** (default — one HTML covers both eyes)::
25
+
26
+ C:\RetCamOutput\
27
+ 105-10-0989-3\
28
+ a1b2c3d4.jpg <- eye image (left or right)
29
+ e5f6a7b8.jpg <- eye image (left or right)
30
+ c9d0e1f2.html <- combined report (both eyes)
31
+ 105-10-0001-2\
32
+ ...
33
+
34
+ **Per-eye report** (one HTML per eye)::
35
+
36
+ C:\RetCamOutput\
37
+ 105-10-0989-3\
38
+ a1b2c3d4.jpg <- eye image (left or right)
39
+ e5f6a7b8.jpg <- eye image (left or right)
40
+ c9d0e1f2.html <- eye report (left or right)
41
+ f3a4b5c6.html <- eye report (left or right)
42
+ 105-10-0001-2\
43
+ ...
44
+
45
+ Because filenames are random UUIDs, the watchdog queries the camera's SQLite database to determine which file belongs to which eye.
46
+
47
+ Processing is triggered once a subject folder contains the expected number
48
+ of files:
49
+
50
+ - **Combined** (default): at least **2 JPEG** and **1 HTML** file (3 files total).
51
+ - **Per-eye**: at least **2 JPEG** and **2 HTML** files (4 files total).
52
+
53
+ Configuration
54
+ -------------
55
+
56
+ All settings live in a single JSON file. Create ``camera_config.json``::
57
+
58
+ {
59
+ "watch_dir": "C:\\RetCamOutput",
60
+ "db_path": "C:\\RetCamOutput\\camera.db",
61
+ "api_url": "https://edc.example.com",
62
+ "token": "YOUR_DRF_TOKEN",
63
+ "device_id": "RET-CAM-001",
64
+ "site_id": "40",
65
+
66
+ "db_patient_table": "patients",
67
+ "db_patient_subject_id": "subject_identifier",
68
+ "db_patient_initials": "initials",
69
+ "db_patient_sex": "sex",
70
+ "db_patient_age": "age",
71
+
72
+ "db_image_table": "images",
73
+ "db_image_subject_id": "subject_identifier",
74
+ "db_image_filename": "filename",
75
+ "db_image_eye": "eye",
76
+
77
+ "report_type": "combined"
78
+ }
79
+
80
+ Required keys
81
+ ~~~~~~~~~~~~~
82
+
83
+ ``watch_dir``
84
+ Folder the camera writes subject subfolders to.
85
+
86
+ ``db_path``
87
+ Path to the camera's SQLite database.
88
+
89
+ ``api_url``
90
+ Base URL of the EDC server (e.g. ``https://edc.example.com``).
91
+
92
+ ``token``
93
+ DRF authentication token for the camera user.
94
+
95
+ Optional keys
96
+ ~~~~~~~~~~~~~
97
+
98
+ ``device_id``
99
+ Identifier for this camera (sent to the server with each session).
100
+
101
+ ``site_id``
102
+ Study site identifier.
103
+
104
+ ``report_type``
105
+ How the camera writes its analysis reports. ``combined`` (default) means
106
+ a single HTML file covers both eyes; ``per_eye`` means one HTML per eye.
107
+ This controls how many files the watchdog expects before triggering an
108
+ upload (3 for combined, 4 for per_eye).
109
+
110
+ ``log_level``
111
+ One of ``DEBUG``, ``INFO`` (default), ``WARNING``, ``ERROR``.
112
+
113
+ Database column mapping
114
+ ~~~~~~~~~~~~~~~~~~~~~~~
115
+
116
+ These keys tell the watchdog which tables and columns to query in the
117
+ camera's SQLite database. Adjust them to match your camera vendor's
118
+ actual schema.
119
+
120
+ +-----------------------------+------------------------------------------+--------------------------+
121
+ | Config key | Purpose | Default |
122
+ +=============================+==========================================+==========================+
123
+ | ``db_patient_table`` | Table containing patient demographics | ``patients`` |
124
+ +-----------------------------+------------------------------------------+--------------------------+
125
+ | ``db_patient_subject_id`` | Column for subject identifier | ``subject_identifier`` |
126
+ +-----------------------------+------------------------------------------+--------------------------+
127
+ | ``db_patient_initials`` | Column for initials | ``initials`` |
128
+ +-----------------------------+------------------------------------------+--------------------------+
129
+ | ``db_patient_sex`` | Column for sex (M/F) | ``sex`` |
130
+ +-----------------------------+------------------------------------------+--------------------------+
131
+ | ``db_patient_age`` | Column for age in years | ``age`` |
132
+ +-----------------------------+------------------------------------------+--------------------------+
133
+ | ``db_image_table`` | Table mapping files to eyes | ``images`` |
134
+ +-----------------------------+------------------------------------------+--------------------------+
135
+ | ``db_image_subject_id`` | Column for subject identifier | ``subject_identifier`` |
136
+ +-----------------------------+------------------------------------------+--------------------------+
137
+ | ``db_image_filename`` | Column for UUID filename | ``filename`` |
138
+ +-----------------------------+------------------------------------------+--------------------------+
139
+ | ``db_image_eye`` | Column for eye laterality | ``eye`` |
140
+ +-----------------------------+------------------------------------------+--------------------------+
141
+
142
+ The watchdog normalises eye values automatically. All of the following
143
+ are recognised:
144
+
145
+ - **Left eye**: ``L``, ``LE``, ``OS``, ``LEFT``
146
+ - **Right eye**: ``R``, ``RE``, ``OD``, ``RIGHT``
147
+
148
+ Usage
149
+ -----
150
+
151
+ With a config file (recommended)::
152
+
153
+ uv run camera_watchdog.py --config camera_config.json
154
+
155
+ CLI flags override any value from the config file::
156
+
157
+ uv run camera_watchdog.py --config camera_config.json --log-level DEBUG --report-type per_eye
158
+
159
+ Without a config file (all flags on the command line)::
160
+
161
+ uv run camera_watchdog.py ^
162
+ --watch-dir C:\RetCamOutput ^
163
+ --db-path C:\RetCamOutput\camera.db ^
164
+ --api-url https://edc.example.com ^
165
+ --token YOUR_TOKEN ^
166
+ --device-id RET-CAM-001 ^
167
+ --db-patient-table Exams ^
168
+ --db-patient-subject-id patient_code ^
169
+ --db-image-table CapturedFiles ^
170
+ --db-image-eye laterality
171
+
172
+ Stop with ``Ctrl+C``.
173
+
174
+ What it does
175
+ ------------
176
+
177
+ The watchdog runs continuously and performs the following for each
178
+ subject:
179
+
180
+ 1. **Detect** — watches for new files in subject subfolders using
181
+ filesystem events (``watchdog``), plus a periodic sweep every 60 seconds as a safety net.
182
+
183
+ 2. **Wait** — each file is given up to 30 seconds to stabilise (stop growing) before being registered, so half-written files from the camera are not picked up prematurely.
184
+
185
+ 3. **Query camera DB** — retrieves the subject's demographics
186
+ (initials, sex, age) and determines which JPEG is the left eye
187
+ and which is the right. In per-eye mode, the HTML reports are also
188
+ mapped to eyes; in combined mode the HTML is uploaded without eye
189
+ mapping.
190
+
191
+ 4. **Resolve** — ``POST /api/retinopathy/resolve/`` validates the
192
+ subject against ``RegisteredSubject`` on the CLINICEDC server and
193
+ creates (or reactivates) a session.
194
+
195
+ 5. **Upload** — sends each file to the correct API endpoint:
196
+
197
+ - ``*.jpg`` + left eye -> ``POST .../left/``
198
+ - ``*.jpg`` + right eye -> ``POST .../right/``
199
+ - ``*.html`` (combined) -> ``POST .../report/``
200
+ - ``*.html`` + left eye (per_eye) -> ``POST .../left_report/``
201
+ - ``*.html`` + right eye (per_eye) -> ``POST .../right_report/``
202
+
203
+ Each upload includes a SHA-256 checksum for integrity verification.
204
+
205
+ 6. **Verify** — ``GET .../status/`` confirms the session received all expected files.
206
+
207
+ 7. **Archive** — the entire subject folder is moved to
208
+ ``<watch-dir>/processed/<subject_id>_<timestamp>/``.
209
+
210
+ Error handling
211
+ --------------
212
+
213
+ - **Retries** — every API call is retried up to 3 times with a
214
+ 5-second delay between attempts.
215
+
216
+ - **Failed subjects** — if any step fails (server unreachable,
217
+ validation error, upload failure), the subject is marked as
218
+ unprocessed and will be retried on the next 60-second sweep.
219
+
220
+ - **Startup scan** — on (re)start the watchdog scans all existing
221
+ subject folders, so a restart after a crash picks up where it left off.
222
+
223
+ - **Thread safety** — file detection and upload run on separate
224
+ threads with proper locking, so multiple subjects can be uploaded concurrently.
225
+
226
+ Inspecting the camera database
227
+ ------------------------------
228
+
229
+ To discover your camera's actual table and column names, open the
230
+ SQLite database and list its schema::
231
+
232
+ sqlite3 camera.db
233
+ .tables
234
+ .schema patients
235
+ .schema images
236
+
237
+ Then update the ``db_*`` keys in your config file to match.
238
+
239
+ .. |pypi| image:: https://img.shields.io/pypi/v/fundus-camera-watchdog.svg
240
+ :target: https://pypi.python.org/pypi/fundus-camera-watchdog
241
+
242
+ .. |actions| image:: https://github.com/erikvw/fundus-camera-watchdog/actions/workflows/build.yml/badge.svg
243
+ :target: https://github.com/erikvw/fundus-camera-watchdog/actions/workflows/build.yml
244
+
245
+ .. |codecov| image:: https://codecov.io/gh/erikvw/fundus-camera-watchdog/branch/develop/graph/badge.svg
246
+ :target: https://codecov.io/gh/erikvw/fundus-camera-watchdog
247
+
248
+ .. |downloads| image:: https://pepy.tech/badge/fundus-camera-watchdog
249
+ :target: https://pepy.tech/project/fundus-camera-watchdog
250
+
251
+ .. |clinicedc| image:: https://img.shields.io/badge/framework-Clinic_EDC-green
252
+ :alt:Made with clinicedc
253
+ :target: https://github.com/clinicedc
@@ -0,0 +1,82 @@
1
+ [project]
2
+ name = "fundus-camera-watchdog"
3
+ version = "0.2.0"
4
+ description = "File watchdog for fundus camera"
5
+ readme = "README.rst"
6
+ requires-python = '>=3.12,<3.15'
7
+ dependencies = [
8
+ "requests>=2.34.2",
9
+ "tzdata>=2026.2",
10
+ "watchdog>=6.0.0",
11
+ ]
12
+
13
+ [project.urls]
14
+ Homepage = "https://github.com/erikvw/fundus-camera-watchdog"
15
+
16
+ [dependency-groups]
17
+ dev = [
18
+ "pytest>=8.0",
19
+ ]
20
+ docs = [
21
+ "furo>=2025.12.19",
22
+ "sphinx>=9.1.0",
23
+ ]
24
+
25
+ [build-system]
26
+ requires = ["uv_build>=0.11.16,<0.12.0"]
27
+ build-backend = "uv_build"
28
+
29
+ [tool.uv.build-backend]
30
+ module-root = "src"
31
+ source-exclude = ["bin"]
32
+ namespace = false
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
36
+
37
+ [tool.ruff]
38
+ line-length = 95
39
+ indent-width = 4
40
+ target-version = "py312"
41
+ exclude = [".tox", ".venv"]
42
+
43
+ [tool.ruff.lint.per-file-ignores]
44
+ "__init__.py" = ["F401"]
45
+
46
+ [tool.ruff.lint.mccabe]
47
+ max-complexity = 12
48
+
49
+ [tool.ruff.format]
50
+ quote-style = "double"
51
+ indent-style = "space"
52
+ skip-magic-trailing-comma = false
53
+ line-ending = "auto"
54
+
55
+ [tool.ruff.lint]
56
+ select = [
57
+ "A",
58
+ "ARG",
59
+ "B",
60
+ "C90",
61
+ "COM",
62
+ "DTZ",
63
+ "E",
64
+ "F",
65
+ "FA",
66
+ "FURB",
67
+ "I",
68
+ "INT",
69
+ "N",
70
+ "PERF",
71
+ "PL",
72
+ "PLR",
73
+ "PTH",
74
+ "RET",
75
+ "RUF",
76
+ # "S",
77
+ "SIM",
78
+ "T20",
79
+ "TRY",
80
+ "UP",
81
+ "W",
82
+ ]