qmenta-client 1.1.dev1492__py3-none-any.whl → 1.1.dev1507__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 +340 -11
- {qmenta_client-1.1.dev1492.dist-info → qmenta_client-1.1.dev1507.dist-info}/METADATA +1 -1
- {qmenta_client-1.1.dev1492.dist-info → qmenta_client-1.1.dev1507.dist-info}/RECORD +4 -4
- {qmenta_client-1.1.dev1492.dist-info → qmenta_client-1.1.dev1507.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
|
|
@@ -259,14 +262,14 @@ class Project:
|
|
|
259
262
|
a power of 2: 2**x. Default value of x is 9 (chunk_size = 512 kB)
|
|
260
263
|
split_data : bool
|
|
261
264
|
If True, the platform will try to split the uploaded file into
|
|
262
|
-
different sessions. It will be ignored when the ssid
|
|
265
|
+
different sessions. It will be ignored when the ssid or a
|
|
266
|
+
add_to_container_id are given.
|
|
263
267
|
|
|
264
268
|
Returns
|
|
265
269
|
-------
|
|
266
270
|
bool
|
|
267
271
|
True if correctly uploaded, False otherwise.
|
|
268
272
|
"""
|
|
269
|
-
|
|
270
273
|
filename = os.path.split(file_path)[1]
|
|
271
274
|
input_data_type = "offline_analysis:1.0" if result else input_data_type
|
|
272
275
|
|
|
@@ -277,6 +280,8 @@ class Project:
|
|
|
277
280
|
|
|
278
281
|
total_bytes = os.path.getsize(file_path)
|
|
279
282
|
|
|
283
|
+
split_data = self.__assert_split_data(split_data, ssid, add_to_container_id)
|
|
284
|
+
|
|
280
285
|
# making chunks of the file and sending one by one
|
|
281
286
|
logger = logging.getLogger(logger_name)
|
|
282
287
|
with open(file_path, "rb") as file_object:
|
|
@@ -293,10 +298,6 @@ class Project:
|
|
|
293
298
|
response = None
|
|
294
299
|
last_chunk = False
|
|
295
300
|
|
|
296
|
-
if ssid and split_data:
|
|
297
|
-
logger.warning("split-data argument will be ignored because" + " ssid has been specified")
|
|
298
|
-
split_data = False
|
|
299
|
-
|
|
300
301
|
while True:
|
|
301
302
|
data = file_object.read(chunk_size)
|
|
302
303
|
if not data:
|
|
@@ -371,8 +372,7 @@ class Project:
|
|
|
371
372
|
logger.error(error)
|
|
372
373
|
return False
|
|
373
374
|
|
|
374
|
-
message = "Your data was successfully uploaded."
|
|
375
|
-
message += "The uploaded file will be soon processed !"
|
|
375
|
+
message = "Your data was successfully uploaded. The uploaded file will be soon processed !"
|
|
376
376
|
logger.info(message)
|
|
377
377
|
return True
|
|
378
378
|
|
|
@@ -2038,6 +2038,232 @@ class Project:
|
|
|
2038
2038
|
|
|
2039
2039
|
return res["guidance_text"]
|
|
2040
2040
|
|
|
2041
|
+
def parse_qc_text(self, patient_id=None, subject_name=None, ssid=None):
|
|
2042
|
+
"""
|
|
2043
|
+
Parse QC (Quality Control) text output into a structured dictionary format.
|
|
2044
|
+
|
|
2045
|
+
This function takes raw QC text output (from the Protocol Adherence analysis)
|
|
2046
|
+
and parses it into a structured format that separates passed and failed rules,
|
|
2047
|
+
along with their associated files and conditions.
|
|
2048
|
+
|
|
2049
|
+
Args:
|
|
2050
|
+
patient_id (str, optional):
|
|
2051
|
+
Patient identifier. Defaults to None.
|
|
2052
|
+
subject_name (str, optional):
|
|
2053
|
+
Subject/patient name. Defaults to None. Mandatory if no patient_id is provided.
|
|
2054
|
+
ssid (str, optional):
|
|
2055
|
+
Session ID. Defaults to None. Mandatory if subject_name is provided.
|
|
2056
|
+
|
|
2057
|
+
Returns:
|
|
2058
|
+
dict: A structured dictionary containing a list of dictionaries with passed rules and their details
|
|
2059
|
+
and failed rules and their details. Details of passed rules are:
|
|
2060
|
+
per each rule: Files that have passed the rule. Per each file name of the file and number of conditions
|
|
2061
|
+
of the rule.
|
|
2062
|
+
Details of failed rules are:
|
|
2063
|
+
- Per each rule failed conditions: Number of times it failed. Each condition status.
|
|
2064
|
+
|
|
2065
|
+
Example:
|
|
2066
|
+
>>> parse_qc_text(subject_name="patient_123", ssid=1)
|
|
2067
|
+
{
|
|
2068
|
+
"passed": [
|
|
2069
|
+
{
|
|
2070
|
+
"rule": "T2",
|
|
2071
|
+
"sub_rule": "rule_15T",
|
|
2072
|
+
"files": [
|
|
2073
|
+
{
|
|
2074
|
+
"file": "path/to/file1",
|
|
2075
|
+
"passed_conditions": 4
|
|
2076
|
+
}
|
|
2077
|
+
]
|
|
2078
|
+
}
|
|
2079
|
+
],
|
|
2080
|
+
"failed": [
|
|
2081
|
+
{
|
|
2082
|
+
"rule": "T1",
|
|
2083
|
+
"files": [
|
|
2084
|
+
{
|
|
2085
|
+
"file": "path/to/file2",
|
|
2086
|
+
"conditions": [
|
|
2087
|
+
{
|
|
2088
|
+
"status": "failed",
|
|
2089
|
+
"condition": "SliceThickness between..."
|
|
2090
|
+
}
|
|
2091
|
+
]
|
|
2092
|
+
}
|
|
2093
|
+
],
|
|
2094
|
+
"failed_conditions": {
|
|
2095
|
+
"SliceThickness between...": 1
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
]
|
|
2099
|
+
}
|
|
2100
|
+
"""
|
|
2101
|
+
|
|
2102
|
+
_, text = self.get_qc_status_subject(patient_id=patient_id, subject_name=subject_name, ssid=ssid)
|
|
2103
|
+
|
|
2104
|
+
result = {"passed": [], "failed": []}
|
|
2105
|
+
|
|
2106
|
+
# Split into failed and passed sections
|
|
2107
|
+
sections = re.split(r"={10,}\n\n", text)
|
|
2108
|
+
if len(sections) == 3:
|
|
2109
|
+
failed_section = sections[1].split("=" * 10)[0].strip()
|
|
2110
|
+
passed_section = sections[2].strip()
|
|
2111
|
+
else:
|
|
2112
|
+
section = sections[1].split("=" * 10)[0].strip()
|
|
2113
|
+
if "PASSED QC MESSAGES" in section:
|
|
2114
|
+
passed_section = section
|
|
2115
|
+
failed_section = ""
|
|
2116
|
+
else:
|
|
2117
|
+
failed_section = section
|
|
2118
|
+
passed_section = ""
|
|
2119
|
+
|
|
2120
|
+
# Parse failed rules
|
|
2121
|
+
failed_rules = re.split(r"\n ❌ ", failed_section)
|
|
2122
|
+
result = self.__parse_fail_rules(failed_rules, result)
|
|
2123
|
+
|
|
2124
|
+
# Parse passed rules
|
|
2125
|
+
passed_rules = re.split(r"\n ✅ ", passed_section)
|
|
2126
|
+
result = self.__parse_pass_rules(passed_rules, result)
|
|
2127
|
+
|
|
2128
|
+
return result
|
|
2129
|
+
|
|
2130
|
+
def calculate_qc_statistics(self):
|
|
2131
|
+
"""
|
|
2132
|
+
Calculate comprehensive statistics from multiple QC results across subjects from a project in the QMENTA
|
|
2133
|
+
platform.
|
|
2134
|
+
|
|
2135
|
+
This function aggregates and analyzes QC results from multiple subjects/containers,
|
|
2136
|
+
providing statistical insights about rule pass/fail rates, file statistics,
|
|
2137
|
+
and condition failure patterns.
|
|
2138
|
+
|
|
2139
|
+
Returns:
|
|
2140
|
+
dict: A dictionary containing comprehensive QC statistics including:
|
|
2141
|
+
- passed_rules: Total count of passed rules across all subjects
|
|
2142
|
+
- failed_rules: Total count of failed rules across all subjects
|
|
2143
|
+
- subjects_passed: Count of subjects with no failed rules
|
|
2144
|
+
- subjects_with_failed: Count of subjects with at least one failed rule
|
|
2145
|
+
- num_passed_files_distribution: Distribution of how many rules have N passed files
|
|
2146
|
+
- file_stats: File-level statistics (total, passed, failed, pass percentage)
|
|
2147
|
+
- condition_failure_rates: Frequency and percentage of each failed condition
|
|
2148
|
+
- rule_success_rates: Success rates for each rule type
|
|
2149
|
+
|
|
2150
|
+
The statistics help identify:
|
|
2151
|
+
- Overall QC pass rates
|
|
2152
|
+
- Most common failure conditions
|
|
2153
|
+
- Rule-specific success rates
|
|
2154
|
+
- Distribution of passed files per rule
|
|
2155
|
+
- Subject-level pass rates
|
|
2156
|
+
|
|
2157
|
+
Example:
|
|
2158
|
+
>>> project.calculate_qc_statistics()
|
|
2159
|
+
{
|
|
2160
|
+
"passed_rules": 42,
|
|
2161
|
+
"failed_rules": 8,
|
|
2162
|
+
"subjects_passed": 15,
|
|
2163
|
+
"subjects_with_failed": 5,
|
|
2164
|
+
"num_passed_files_distribution": {
|
|
2165
|
+
"1": 30,
|
|
2166
|
+
"2": 12
|
|
2167
|
+
},
|
|
2168
|
+
"file_stats": {
|
|
2169
|
+
"total": 50,
|
|
2170
|
+
"passed": 45,
|
|
2171
|
+
"failed": 5,
|
|
2172
|
+
"pass_percentage": 90.0
|
|
2173
|
+
},
|
|
2174
|
+
"condition_failure_rates": {
|
|
2175
|
+
"SliceThickness": {
|
|
2176
|
+
"count": 5,
|
|
2177
|
+
"percentage": 62.5
|
|
2178
|
+
}
|
|
2179
|
+
},
|
|
2180
|
+
"rule_success_rates": {
|
|
2181
|
+
"T1": {
|
|
2182
|
+
"passed": 20,
|
|
2183
|
+
"failed": 2,
|
|
2184
|
+
"success_rate": 90.91
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
"""
|
|
2189
|
+
qc_results_list = list()
|
|
2190
|
+
containers = self.list_input_containers()
|
|
2191
|
+
|
|
2192
|
+
for c in containers:
|
|
2193
|
+
qc_results_list.append(self.parse_qc_text(subject_name=c["patient_secret_name"], ssid=c["ssid"]))
|
|
2194
|
+
|
|
2195
|
+
# Initialize statistics
|
|
2196
|
+
stats = {
|
|
2197
|
+
"passed_rules": 0,
|
|
2198
|
+
"failed_rules": 0,
|
|
2199
|
+
"subjects_passed": 0,
|
|
2200
|
+
"subjects_with_failed": 0,
|
|
2201
|
+
"num_passed_files_distribution": defaultdict(int), # How many rules have N passed files
|
|
2202
|
+
"file_stats": {"total": 0, "passed": 0, "failed": 0, "pass_percentage": 0.0},
|
|
2203
|
+
"condition_failure_rates": defaultdict(lambda: {"count": 0, "percentage": 0.0}),
|
|
2204
|
+
"rule_success_rates": defaultdict(lambda: {"passed": 0, "failed": 0, "success_rate": 0.0}),
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
total_failures = 0
|
|
2208
|
+
|
|
2209
|
+
# sum subjects with not failed qc message
|
|
2210
|
+
stats["subjects_passed"] = sum([1 for rules in qc_results_list if not rules["failed"]])
|
|
2211
|
+
# sum subjects with some failed qc message
|
|
2212
|
+
stats["subjects_with_failed"] = sum([1 for rules in qc_results_list if rules["failed"]])
|
|
2213
|
+
# sum rules that have passed
|
|
2214
|
+
stats["passed_rules"] = sum([len(rules["passed"]) for rules in qc_results_list if rules["failed"]])
|
|
2215
|
+
# sum rules that have failed
|
|
2216
|
+
stats["failed_rules"] = sum([len(rules["failed"]) for rules in qc_results_list if rules["failed"]])
|
|
2217
|
+
|
|
2218
|
+
for qc_results in qc_results_list:
|
|
2219
|
+
|
|
2220
|
+
# Count passed files distribution
|
|
2221
|
+
for rule in qc_results["passed"]:
|
|
2222
|
+
num_files = len(rule["files"])
|
|
2223
|
+
stats["num_passed_files_distribution"][num_files] += 1
|
|
2224
|
+
stats["file_stats"]["passed"] += len(rule["files"])
|
|
2225
|
+
stats["file_stats"]["total"] += len(rule["files"])
|
|
2226
|
+
rule_name = rule["rule"]
|
|
2227
|
+
stats["rule_success_rates"][rule_name]["passed"] += 1
|
|
2228
|
+
|
|
2229
|
+
for rule in qc_results["failed"]:
|
|
2230
|
+
stats["file_stats"]["total"] += len(rule["files"])
|
|
2231
|
+
stats["file_stats"]["failed"] += len(rule["files"])
|
|
2232
|
+
for condition, count in rule["failed_conditions"].items():
|
|
2233
|
+
# Extract just the condition text without actual value
|
|
2234
|
+
clean_condition = re.sub(r"\.\s*Actual value:.*$", "", condition)
|
|
2235
|
+
stats["condition_failure_rates"][clean_condition]["count"] += count
|
|
2236
|
+
total_failures += count
|
|
2237
|
+
rule_name = rule["rule"]
|
|
2238
|
+
stats["rule_success_rates"][rule_name]["failed"] += 1
|
|
2239
|
+
|
|
2240
|
+
if stats["file_stats"]["total"] > 0:
|
|
2241
|
+
stats["file_stats"]["pass_percentage"] = round(
|
|
2242
|
+
(stats["file_stats"]["passed"] / stats["file_stats"]["total"]) * 100, 2
|
|
2243
|
+
)
|
|
2244
|
+
|
|
2245
|
+
# Calculate condition failure percentages
|
|
2246
|
+
for condition in stats["condition_failure_rates"]:
|
|
2247
|
+
if total_failures > 0:
|
|
2248
|
+
stats["condition_failure_rates"][condition]["percentage"] = round(
|
|
2249
|
+
(stats["condition_failure_rates"][condition]["count"] / total_failures) * 100, 2
|
|
2250
|
+
)
|
|
2251
|
+
|
|
2252
|
+
# Calculate rule success rates
|
|
2253
|
+
for rule in stats["rule_success_rates"]:
|
|
2254
|
+
total = stats["rule_success_rates"][rule]["passed"] + stats["rule_success_rates"][rule]["failed"]
|
|
2255
|
+
if total > 0:
|
|
2256
|
+
stats["rule_success_rates"][rule]["success_rate"] = round(
|
|
2257
|
+
(stats["rule_success_rates"][rule]["passed"] / total) * 100, 2
|
|
2258
|
+
)
|
|
2259
|
+
|
|
2260
|
+
# Convert defaultdict to regular dict for cleaner JSON output
|
|
2261
|
+
stats["num_passed_files_distribution"] = dict(stats["num_passed_files_distribution"])
|
|
2262
|
+
stats["condition_failure_rates"] = dict(stats["condition_failure_rates"])
|
|
2263
|
+
stats["rule_success_rates"] = dict(stats["rule_success_rates"])
|
|
2264
|
+
|
|
2265
|
+
return stats
|
|
2266
|
+
|
|
2041
2267
|
""" Helper Methods """
|
|
2042
2268
|
|
|
2043
2269
|
def __handle_start_analysis(self, post_data, ignore_warnings=False, ignore_file_selection=True, n_calls=0):
|
|
@@ -2143,7 +2369,6 @@ class Project:
|
|
|
2143
2369
|
elif post_data.get("cancel"):
|
|
2144
2370
|
continue
|
|
2145
2371
|
|
|
2146
|
-
number_of_files_to_select = 1
|
|
2147
2372
|
if filter_data["range"][0] != 0:
|
|
2148
2373
|
number_of_files_to_select = filter_data["range"][0]
|
|
2149
2374
|
elif filter_data["range"][1] != 0:
|
|
@@ -2162,7 +2387,7 @@ class Project:
|
|
|
2162
2387
|
logger.warning(
|
|
2163
2388
|
f" · File filter name: '{filter_key}'. Type "
|
|
2164
2389
|
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 ''}."
|
|
2390
|
+
f"{'s (i.e., file1.zip, file2.zip, file3.zip)' if number_of_files_to_select > 1 else ''}."
|
|
2166
2391
|
)
|
|
2167
2392
|
save_file_ids, select_file_filter = {}, ""
|
|
2168
2393
|
for file_ in filter_data["files"]:
|
|
@@ -2365,3 +2590,107 @@ class Project:
|
|
|
2365
2590
|
value.replace(d_type + ";", "")
|
|
2366
2591
|
file_metadata[d_tag] = {"operation": "in-list", "value": value.replace(d_type + ";", "").split(";")}
|
|
2367
2592
|
return modality, tags, file_metadata
|
|
2593
|
+
|
|
2594
|
+
def __assert_split_data(self, split_data, ssid, add_to_container_id):
|
|
2595
|
+
"""
|
|
2596
|
+
Assert if the split_data parameter is possible to use in regards
|
|
2597
|
+
to the ssid and add_to_container_id parameters during upload.
|
|
2598
|
+
Changes its status to False if needed.
|
|
2599
|
+
|
|
2600
|
+
Parameters
|
|
2601
|
+
----------
|
|
2602
|
+
split_data : Bool
|
|
2603
|
+
split_data parameter from method 'upload_file'.
|
|
2604
|
+
ssid : str
|
|
2605
|
+
Session ID.
|
|
2606
|
+
add_to_container_id : int or bool
|
|
2607
|
+
Container ID or False
|
|
2608
|
+
|
|
2609
|
+
Returns
|
|
2610
|
+
-------
|
|
2611
|
+
split_data : Bool
|
|
2612
|
+
|
|
2613
|
+
"""
|
|
2614
|
+
|
|
2615
|
+
logger = logging.getLogger(logger_name)
|
|
2616
|
+
if ssid and split_data:
|
|
2617
|
+
logger.warning("split-data argument will be ignored because ssid has been specified")
|
|
2618
|
+
split_data = False
|
|
2619
|
+
|
|
2620
|
+
if add_to_container_id and split_data:
|
|
2621
|
+
logger.warning("split-data argument will be ignored because add_to_container_id has been specified")
|
|
2622
|
+
split_data = False
|
|
2623
|
+
|
|
2624
|
+
return split_data
|
|
2625
|
+
|
|
2626
|
+
def __parse_fail_rules(self, failed_rules, result):
|
|
2627
|
+
"""
|
|
2628
|
+
Parse fail rules.
|
|
2629
|
+
"""
|
|
2630
|
+
|
|
2631
|
+
for rule_text in failed_rules[1:]: # Skip first empty part
|
|
2632
|
+
rule_name = rule_text.split(" ❌")[0].strip()
|
|
2633
|
+
rule_data = {"rule": rule_name, "files": [], "failed_conditions": {}}
|
|
2634
|
+
|
|
2635
|
+
# Extract all file comparisons for this rule
|
|
2636
|
+
file_comparisons = re.split(r"\t- Comparison with file:", rule_text)
|
|
2637
|
+
for comp in file_comparisons[1:]: # Skip first part
|
|
2638
|
+
file_name = comp.split("\n")[0].strip()
|
|
2639
|
+
conditions_match = re.search(r"Conditions:(.*?)(?=\n\t- Comparison|\n\n|$)", comp, re.DOTALL)
|
|
2640
|
+
if not conditions_match:
|
|
2641
|
+
continue
|
|
2642
|
+
|
|
2643
|
+
conditions_text = conditions_match.group(1).strip()
|
|
2644
|
+
# Parse conditions
|
|
2645
|
+
conditions = []
|
|
2646
|
+
for line in conditions_text.split("\n"):
|
|
2647
|
+
line = line.strip()
|
|
2648
|
+
if line.startswith("·"):
|
|
2649
|
+
status = "✔" if "✔" in line else "🚫"
|
|
2650
|
+
condition = re.sub(r"^· [✔🚫]\s*", "", line)
|
|
2651
|
+
conditions.append({"status": "passed" if status == "✔" else "failed", "condition": condition})
|
|
2652
|
+
|
|
2653
|
+
# Add to failed conditions summary
|
|
2654
|
+
for cond in conditions:
|
|
2655
|
+
if cond["status"] == "failed":
|
|
2656
|
+
cond_text = cond["condition"]
|
|
2657
|
+
if cond_text not in rule_data["failed_conditions"]:
|
|
2658
|
+
rule_data["failed_conditions"][cond_text] = 0
|
|
2659
|
+
rule_data["failed_conditions"][cond_text] += 1
|
|
2660
|
+
|
|
2661
|
+
rule_data["files"].append({"file": file_name, "conditions": conditions})
|
|
2662
|
+
|
|
2663
|
+
result["failed"].append(rule_data)
|
|
2664
|
+
return result
|
|
2665
|
+
|
|
2666
|
+
def __parse_pass_rules(self, passed_rules, result):
|
|
2667
|
+
"""
|
|
2668
|
+
Parse pass rules.
|
|
2669
|
+
"""
|
|
2670
|
+
|
|
2671
|
+
for rule_text in passed_rules[1:]: # Skip first empty part
|
|
2672
|
+
rule_name = rule_text.split(" ✅")[0].strip()
|
|
2673
|
+
rule_data = {"rule": rule_name, "sub_rule": None, "files": []}
|
|
2674
|
+
|
|
2675
|
+
# Get sub-rule
|
|
2676
|
+
sub_rule_match = re.search(r"Sub-rule: (.*?)\n", rule_text)
|
|
2677
|
+
if sub_rule_match:
|
|
2678
|
+
rule_data["sub_rule"] = sub_rule_match.group(1).strip()
|
|
2679
|
+
|
|
2680
|
+
# Get files passed
|
|
2681
|
+
files_passed = re.search(r"List of files passed:(.*?)(?=\n\n|\Z)", rule_text, re.DOTALL)
|
|
2682
|
+
if files_passed:
|
|
2683
|
+
for line in files_passed.group(1).split("\n"):
|
|
2684
|
+
line = line.strip()
|
|
2685
|
+
if line.startswith("·"):
|
|
2686
|
+
file_match = re.match(r"· (.*?) \((\d+)/(\d+)\)", line)
|
|
2687
|
+
if file_match:
|
|
2688
|
+
rule_data["files"].append(
|
|
2689
|
+
{
|
|
2690
|
+
"file": file_match.group(1).strip(),
|
|
2691
|
+
"passed_conditions": int(file_match.group(2)),
|
|
2692
|
+
}
|
|
2693
|
+
)
|
|
2694
|
+
|
|
2695
|
+
result["passed"].append(rule_data)
|
|
2696
|
+
return result
|
|
@@ -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=pV9mW90BzPIMOFauPiAONBIsYJfGsbi_Xabbe9DW32U,100493
|
|
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.dev1507.dist-info/METADATA,sha256=hGQLiIggQT0NX8tOF8VIklAu9VdsOSldflacTNBIo9g,672
|
|
9
|
+
qmenta_client-1.1.dev1507.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
10
|
+
qmenta_client-1.1.dev1507.dist-info/RECORD,,
|
|
File without changes
|