qmenta-client 1.1.dev1468__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 +317 -9
- {qmenta_client-1.1.dev1468.dist-info → qmenta_client-1.1.dev1504.dist-info}/METADATA +1 -1
- {qmenta_client-1.1.dev1468.dist-info → qmenta_client-1.1.dev1504.dist-info}/RECORD +4 -4
- {qmenta_client-1.1.dev1468.dist-info → qmenta_client-1.1.dev1504.dist-info}/WHEEL +0 -0
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
|
|
@@ -32,7 +35,7 @@ def convert_qc_value_to_qcstatus(value):
|
|
|
32
35
|
Returns
|
|
33
36
|
-------
|
|
34
37
|
QCStatus or Bool
|
|
35
|
-
QCStatus.PASS, QCStatus.FAIL or QCStatus.UNDETERMINED
|
|
38
|
+
QCStatus.PASS, QCStatus.FAIL or QCStatus.UNDETERMINED.
|
|
36
39
|
False if the Value cannot be convered.
|
|
37
40
|
"""
|
|
38
41
|
logger = logging.getLogger(logger_name)
|
|
@@ -226,9 +229,9 @@ class Project:
|
|
|
226
229
|
description="",
|
|
227
230
|
result=False,
|
|
228
231
|
name="",
|
|
229
|
-
input_data_type="
|
|
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
|
|
|
@@ -1937,7 +1940,7 @@ class Project:
|
|
|
1937
1940
|
session = self.get_subjects_metadata(
|
|
1938
1941
|
search_criteria={
|
|
1939
1942
|
"pars_patient_secret_name": f"string;{subject_name}",
|
|
1940
|
-
"pars_ssid": f"integer;eq|{ssid}",
|
|
1943
|
+
"pars_ssid": f"integer;eq|{ssid}" if str(ssid).isdigit() else f"string;{ssid}",
|
|
1941
1944
|
}
|
|
1942
1945
|
)
|
|
1943
1946
|
if len(session) < 1:
|
|
@@ -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,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=
|
|
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.
|
|
9
|
-
qmenta_client-1.1.
|
|
10
|
-
qmenta_client-1.1.
|
|
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,,
|
|
File without changes
|