qmenta-client 1.1.dev1492__py3-none-any.whl → 1.1.dev1504__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.
qmenta/client/Project.py CHANGED
@@ -4,14 +4,17 @@ import hashlib
4
4
  import json
5
5
  import logging
6
6
  import os
7
+ import re
7
8
  import sys
8
9
  import time
10
+ from collections import defaultdict
9
11
  from enum import Enum
10
12
 
11
- from qmenta.client import Account
12
13
  from qmenta.core import errors
13
14
  from qmenta.core import platform
14
15
 
16
+ from qmenta.client import Account
17
+
15
18
  if sys.version_info[0] == 3:
16
19
  # Note: this branch & variable is only needed for python 2/3 compatibility
17
20
  unicode = str
@@ -228,7 +231,7 @@ class Project:
228
231
  name="",
229
232
  input_data_type="qmenta_medical_image_data:3.10",
230
233
  add_to_container_id=0,
231
- chunk_size=2**9,
234
+ chunk_size=2 ** 9,
232
235
  split_data=False,
233
236
  ):
234
237
  """
@@ -474,7 +477,7 @@ class Project:
474
477
  self._account.auth, "file_manager/download_file", data=params, stream=True
475
478
  ) as response, open(local_filename, "wb") as f:
476
479
 
477
- for chunk in response.iter_content(chunk_size=2**9 * 1024):
480
+ for chunk in response.iter_content(chunk_size=2 ** 9 * 1024):
478
481
  f.write(chunk)
479
482
  f.flush()
480
483
 
@@ -520,7 +523,7 @@ class Project:
520
523
  self._account.auth, "file_manager/download_file", data=params, stream=True
521
524
  ) as response, open(zip_name, "wb") as f:
522
525
 
523
- for chunk in response.iter_content(chunk_size=2**9 * 1024):
526
+ for chunk in response.iter_content(chunk_size=2 ** 9 * 1024):
524
527
  f.write(chunk)
525
528
  f.flush()
526
529
 
@@ -2038,6 +2041,312 @@ class Project:
2038
2041
 
2039
2042
  return res["guidance_text"]
2040
2043
 
2044
+ def parse_qc_text(self, patient_id=None, subject_name=None, ssid=None):
2045
+ """
2046
+ Parse QC (Quality Control) text output into a structured dictionary format.
2047
+
2048
+ This function takes raw QC text output (typically from medical imaging quality checks)
2049
+ and parses it into a structured format that separates passed and failed rules,
2050
+ along with their associated files and conditions.
2051
+
2052
+ Args:
2053
+ patient_id (str, optional):
2054
+ Patient identifier. Defaults to None.
2055
+ subject_name (str, optional):
2056
+ Subject/patient name. Defaults to None. Mandatory if no patient_id is provided.
2057
+ ssid (str, optional):
2058
+ Session ID. Defaults to None. Mandatory if subject_name is provided.
2059
+
2060
+ Returns:
2061
+ dict: A structured dictionary containing a list of dictionaries with passed rules and their details
2062
+ and failed rules and their details. Details of passed rules are:
2063
+ per each rule: Files that have passed the rule. Per each file name of the file and number of conditions
2064
+ of the rule.
2065
+ Details of failed rules are:
2066
+ - Per each rule failed conditions: Number of times it failed. Each condition status.
2067
+
2068
+ Example:
2069
+ >>> parse_qc_text(subject_name="patient_123", ssid=1)
2070
+ {
2071
+ "passed": [
2072
+ {
2073
+ "rule": "T2",
2074
+ "sub_rule": "rule_15T",
2075
+ "files": [
2076
+ {
2077
+ "file": "path/to/file1",
2078
+ "passed_conditions": 4
2079
+ }
2080
+ ]
2081
+ }
2082
+ ],
2083
+ "failed": [
2084
+ {
2085
+ "rule": "T1",
2086
+ "files": [
2087
+ {
2088
+ "file": "path/to/file2",
2089
+ "conditions": [
2090
+ {
2091
+ "status": "failed",
2092
+ "condition": "SliceThickness between..."
2093
+ }
2094
+ ]
2095
+ }
2096
+ ],
2097
+ "failed_conditions": {
2098
+ "SliceThickness between...": 1
2099
+ }
2100
+ }
2101
+ ]
2102
+ }
2103
+ """
2104
+
2105
+ _, text = self.get_qc_status_subject(patient_id=patient_id, subject_name=subject_name, ssid=ssid)
2106
+
2107
+ result = {
2108
+ "passed": [],
2109
+ "failed": []
2110
+ }
2111
+
2112
+ # Split into failed and passed sections
2113
+ sections = re.split(r'={10,}\n\n', text)
2114
+ if len(sections) == 3:
2115
+ failed_section = sections[1].split('=' * 10)[0].strip()
2116
+ passed_section = sections[2].strip()
2117
+ else:
2118
+ section = sections[1].split('=' * 10)[0].strip()
2119
+ if "PASSED QC MESSAGES" in section:
2120
+ passed_section = section
2121
+ failed_section = ""
2122
+ else:
2123
+ failed_section = section
2124
+ passed_section = ""
2125
+
2126
+ # Parse failed rules
2127
+ failed_rules = re.split(r'\n ❌ ', failed_section)
2128
+ for rule_text in failed_rules[1:]: # Skip first empty part
2129
+ rule_name = rule_text.split(' ❌')[0].strip()
2130
+ rule_data = {
2131
+ "rule": rule_name,
2132
+ "files": [],
2133
+ "failed_conditions": {}
2134
+ }
2135
+
2136
+ # Extract all file comparisons for this rule
2137
+ file_comparisons = re.split(r'\t- Comparison with file:', rule_text)
2138
+ for comp in file_comparisons[1:]: # Skip first part
2139
+ file_name = comp.split('\n')[0].strip()
2140
+ conditions_match = re.search(
2141
+ r'Conditions:(.*?)(?=\n\t- Comparison|\n\n|$)',
2142
+ comp,
2143
+ re.DOTALL
2144
+ )
2145
+ if not conditions_match:
2146
+ continue
2147
+
2148
+ conditions_text = conditions_match.group(1).strip()
2149
+ # Parse conditions
2150
+ conditions = []
2151
+ for line in conditions_text.split('\n'):
2152
+ line = line.strip()
2153
+ if line.startswith('·'):
2154
+ status = '✔' if '✔' in line else '🚫'
2155
+ condition = re.sub(r'^· [✔🚫]\s*', '', line)
2156
+ conditions.append({
2157
+ "status": "passed" if status == '✔' else "failed",
2158
+ "condition": condition
2159
+ })
2160
+
2161
+ # Add to failed conditions summary
2162
+ for cond in conditions:
2163
+ if cond['status'] == 'failed':
2164
+ cond_text = cond['condition']
2165
+ if cond_text not in rule_data['failed_conditions']:
2166
+ rule_data['failed_conditions'][cond_text] = 0
2167
+ rule_data['failed_conditions'][cond_text] += 1
2168
+
2169
+ rule_data['files'].append({
2170
+ "file": file_name,
2171
+ "conditions": conditions
2172
+ })
2173
+
2174
+ result['failed'].append(rule_data)
2175
+
2176
+ # Parse passed rules
2177
+ passed_rules = re.split(r'\n ✅ ', passed_section)
2178
+ for rule_text in passed_rules[1:]: # Skip first empty part
2179
+ rule_name = rule_text.split(' ✅')[0].strip()
2180
+ rule_data = {
2181
+ "rule": rule_name,
2182
+ "sub_rule": None,
2183
+ "files": []
2184
+ }
2185
+
2186
+ # Get sub-rule
2187
+ sub_rule_match = re.search(r'Sub-rule: (.*?)\n', rule_text)
2188
+ if sub_rule_match:
2189
+ rule_data['sub_rule'] = sub_rule_match.group(1).strip()
2190
+
2191
+ # Get files passed
2192
+ files_passed = re.search(r'List of files passed:(.*?)(?=\n\n|\Z)', rule_text, re.DOTALL)
2193
+ if files_passed:
2194
+ for line in files_passed.group(1).split('\n'):
2195
+ line = line.strip()
2196
+ if line.startswith('·'):
2197
+ file_match = re.match(r'· (.*?) \((\d+)/(\d+)\)', line)
2198
+ if file_match:
2199
+ rule_data['files'].append({
2200
+ "file": file_match.group(1).strip(),
2201
+ "passed_conditions": int(file_match.group(2)),
2202
+ })
2203
+
2204
+ result['passed'].append(rule_data)
2205
+
2206
+ return result
2207
+
2208
+ def calculate_qc_statistics(self):
2209
+ """
2210
+ Calculate comprehensive statistics from multiple QC results across subjects from a project in the QMENTA
2211
+ platform.
2212
+
2213
+ This function aggregates and analyzes QC results from multiple subjects/containers,
2214
+ providing statistical insights about rule pass/fail rates, file statistics,
2215
+ and condition failure patterns.
2216
+
2217
+ Returns:
2218
+ dict: A dictionary containing comprehensive QC statistics including:
2219
+ - passed_rules: Total count of passed rules across all subjects
2220
+ - failed_rules: Total count of failed rules across all subjects
2221
+ - subjects_passed: Count of subjects with no failed rules
2222
+ - subjects_with_failed: Count of subjects with at least one failed rule
2223
+ - num_passed_files_distribution: Distribution of how many rules have N passed files
2224
+ - file_stats: File-level statistics (total, passed, failed, pass percentage)
2225
+ - condition_failure_rates: Frequency and percentage of each failed condition
2226
+ - rule_success_rates: Success rates for each rule type
2227
+
2228
+ The statistics help identify:
2229
+ - Overall QC pass rates
2230
+ - Most common failure conditions
2231
+ - Rule-specific success rates
2232
+ - Distribution of passed files per rule
2233
+ - Subject-level pass rates
2234
+
2235
+ Example:
2236
+ >>> project.calculate_qc_statistics()
2237
+ {
2238
+ "passed_rules": 42,
2239
+ "failed_rules": 8,
2240
+ "subjects_passed": 15,
2241
+ "subjects_with_failed": 5,
2242
+ "num_passed_files_distribution": {
2243
+ "1": 30,
2244
+ "2": 12
2245
+ },
2246
+ "file_stats": {
2247
+ "total": 50,
2248
+ "passed": 45,
2249
+ "failed": 5,
2250
+ "pass_percentage": 90.0
2251
+ },
2252
+ "condition_failure_rates": {
2253
+ "SliceThickness": {
2254
+ "count": 5,
2255
+ "percentage": 62.5
2256
+ }
2257
+ },
2258
+ "rule_success_rates": {
2259
+ "T1": {
2260
+ "passed": 20,
2261
+ "failed": 2,
2262
+ "success_rate": 90.91
2263
+ }
2264
+ }
2265
+ }
2266
+ """
2267
+ qc_results_list = list()
2268
+ containers = self.list_input_containers()
2269
+
2270
+ for c in containers:
2271
+ qc_results_list.append(self.parse_qc_text(subject_name=c["patient_secret_name"], ssid=c["ssid"]))
2272
+
2273
+ # Initialize statistics
2274
+ stats = {
2275
+ 'passed_rules': 0,
2276
+ 'failed_rules': 0,
2277
+ "subjects_passed": 0,
2278
+ "subjects_with_failed": 0,
2279
+ 'num_passed_files_distribution': defaultdict(int), # How many rules have N passed files
2280
+ 'file_stats': {
2281
+ 'total': 0,
2282
+ 'passed': 0,
2283
+ 'failed': 0,
2284
+ 'pass_percentage': 0.0
2285
+ },
2286
+ 'condition_failure_rates': defaultdict(lambda: {'count': 0, 'percentage': 0.0}),
2287
+ 'rule_success_rates': defaultdict(lambda: {'passed': 0, 'failed': 0, 'success_rate': 0.0}),
2288
+ }
2289
+
2290
+ total_failures = 0
2291
+
2292
+ # sum subjects with not failed qc message
2293
+ stats["subjects_passed"] = sum([1 for rules in qc_results_list if not rules["failed"]])
2294
+ # sum subjects with some failed qc message
2295
+ stats["subjects_with_failed"] = sum([1 for rules in qc_results_list if rules["failed"]])
2296
+ # sum rules that have passed
2297
+ stats["passed_rules"] = sum([len(rules['passed']) for rules in qc_results_list if rules["failed"]])
2298
+ # sum rules that have failed
2299
+ stats["failed_rules"] = sum([len(rules['failed']) for rules in qc_results_list if rules["failed"]])
2300
+
2301
+ for qc_results in qc_results_list:
2302
+
2303
+ # Count passed files distribution
2304
+ for rule in qc_results['passed']:
2305
+ num_files = len(rule['files'])
2306
+ stats['num_passed_files_distribution'][num_files] += 1
2307
+ stats['file_stats']['passed'] += len(rule['files'])
2308
+ stats['file_stats']['total'] += len(rule['files'])
2309
+ rule_name = rule['rule']
2310
+ stats['rule_success_rates'][rule_name]['passed'] += 1
2311
+
2312
+ for rule in qc_results['failed']:
2313
+ stats['file_stats']['total'] += len(rule['files'])
2314
+ stats['file_stats']['failed'] += len(rule['files'])
2315
+ for condition, count in rule['failed_conditions'].items():
2316
+ # Extract just the condition text without actual value
2317
+ clean_condition = re.sub(r'\.\s*Actual value:.*$', '', condition)
2318
+ stats['condition_failure_rates'][clean_condition]['count'] += count
2319
+ total_failures += count
2320
+ rule_name = rule['rule']
2321
+ stats['rule_success_rates'][rule_name]['failed'] += 1
2322
+
2323
+ if stats['file_stats']['total'] > 0:
2324
+ stats['file_stats']['pass_percentage'] = round(
2325
+ (stats['file_stats']['passed'] / stats['file_stats']['total']) * 100, 2
2326
+ )
2327
+
2328
+ # Calculate condition failure percentages
2329
+ for condition in stats['condition_failure_rates']:
2330
+ if total_failures > 0:
2331
+ stats['condition_failure_rates'][condition]['percentage'] = round(
2332
+ (stats['condition_failure_rates'][condition]['count'] / total_failures) * 100, 2
2333
+ )
2334
+
2335
+ # Calculate rule success rates
2336
+ for rule in stats['rule_success_rates']:
2337
+ total = stats['rule_success_rates'][rule]['passed'] + stats['rule_success_rates'][rule]['failed']
2338
+ if total > 0:
2339
+ stats['rule_success_rates'][rule]['success_rate'] = round(
2340
+ (stats['rule_success_rates'][rule]['passed'] / total) * 100, 2
2341
+ )
2342
+
2343
+ # Convert defaultdict to regular dict for cleaner JSON output
2344
+ stats['num_passed_files_distribution'] = dict(stats['num_passed_files_distribution'])
2345
+ stats['condition_failure_rates'] = dict(stats['condition_failure_rates'])
2346
+ stats['rule_success_rates'] = dict(stats['rule_success_rates'])
2347
+
2348
+ return stats
2349
+
2041
2350
  """ Helper Methods """
2042
2351
 
2043
2352
  def __handle_start_analysis(self, post_data, ignore_warnings=False, ignore_file_selection=True, n_calls=0):
@@ -2143,7 +2452,6 @@ class Project:
2143
2452
  elif post_data.get("cancel"):
2144
2453
  continue
2145
2454
 
2146
- number_of_files_to_select = 1
2147
2455
  if filter_data["range"][0] != 0:
2148
2456
  number_of_files_to_select = filter_data["range"][0]
2149
2457
  elif filter_data["range"][1] != 0:
@@ -2162,7 +2470,7 @@ class Project:
2162
2470
  logger.warning(
2163
2471
  f" · File filter name: '{filter_key}'. Type "
2164
2472
  f"{number_of_files_to_select} file"
2165
- f"{'s (i.e., file1.zip, file2.zip, file3.zip)' if number_of_files_to_select >1 else ''}."
2473
+ f"{'s (i.e., file1.zip, file2.zip, file3.zip)' if number_of_files_to_select > 1 else ''}."
2166
2474
  )
2167
2475
  save_file_ids, select_file_filter = {}, ""
2168
2476
  for file_ in filter_data["files"]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qmenta-client
3
- Version: 1.1.dev1492
3
+ Version: 1.1.dev1504
4
4
  Summary: Python client lib to interact with the QMENTA platform.
5
5
  Author: QMENTA
6
6
  Author-email: dev@qmenta.com
@@ -1,10 +1,10 @@
1
1
  qmenta/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
2
2
  qmenta/client/Account.py,sha256=7BOWHtRbHdfpBYQqv9v2m2Fag13pExZSxFsjDA7UsW0,9500
3
3
  qmenta/client/File.py,sha256=iCrzrd7rIfjjW2AgMgUoK-ZF2wf-95wCcPKxKw6PGyg,4816
4
- qmenta/client/Project.py,sha256=JQZc5BMWSdPEMGBp1gsZFbhjSbFg-0AoBJFXakddojw,86466
4
+ qmenta/client/Project.py,sha256=vx1GabCiVG7gYqnkNmqhqRyzy7ml7eT4jjnUciBq_Gw,99565
5
5
  qmenta/client/Subject.py,sha256=b5sg9UFtn11bmPM-xFXP8aehOm_HGxnhgT7IPKbrZnE,8688
6
6
  qmenta/client/__init__.py,sha256=Mtqe4zf8n3wuwMXSALENQgp5atQY5VcsyXWs2hjBs28,133
7
7
  qmenta/client/utils.py,sha256=vWUAW0r9yDetdlwNo86sdzKn03FNGvwa7D9UtOA3TEc,2419
8
- qmenta_client-1.1.dev1492.dist-info/METADATA,sha256=SSKZb9aDxY8X5qXEwBYyrDBLpU765M_7Z3zOJWC7UdM,672
9
- qmenta_client-1.1.dev1492.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
10
- qmenta_client-1.1.dev1492.dist-info/RECORD,,
8
+ qmenta_client-1.1.dev1504.dist-info/METADATA,sha256=iZHBMCXur-LBBJuvAnmSpRVX4Q0vkq7U32VcH4TGy_g,672
9
+ qmenta_client-1.1.dev1504.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
10
+ qmenta_client-1.1.dev1504.dist-info/RECORD,,