pythonflex 0.1.2__py3-none-any.whl → 0.1.3__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.
- pythonflex/__init__.py +18 -0
- pythonflex/analysis.py +1299 -0
- pythonflex/data/dataset/liver_cell_lines_500_genes.csv +501 -0
- pythonflex/data/dataset/melanoma_cell_lines_500_genes.csv +501 -0
- pythonflex/data/dataset/neuroblastoma_cell_lines_500_genes.csv +501 -0
- pythonflex/data/gold_standard/CORUM.parquet +0 -0
- pythonflex/data/gold_standard/GOBP.parquet +0 -0
- pythonflex/data/gold_standard/PATHWAY.parquet +0 -0
- pythonflex/data/gold_standard/corum.csv +2917 -0
- pythonflex/data/gold_standard/gobp.csv +4829 -0
- pythonflex/data/gold_standard/pathway.csv +1330 -0
- pythonflex/examples/basic_usage.py +108 -0
- pythonflex/examples/dataset_filtering.py +29 -0
- pythonflex/logging_config.py +56 -0
- pythonflex/plotting.py +510 -0
- pythonflex/preprocessing.py +221 -0
- pythonflex/utils.py +100 -0
- {pythonflex-0.1.2.dist-info → pythonflex-0.1.3.dist-info}/METADATA +1 -1
- pythonflex-0.1.3.dist-info/RECORD +21 -0
- pythonflex-0.1.2.dist-info/RECORD +0 -4
- {pythonflex-0.1.2.dist-info → pythonflex-0.1.3.dist-info}/WHEEL +0 -0
- {pythonflex-0.1.2.dist-info → pythonflex-0.1.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
from .utils import dsave, dload
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
from .logging_config import log
|
|
7
|
+
from importlib import resources
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
tqdm.pandas()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_example_data_path(filename: str):
|
|
13
|
+
return resources.files("pyflex.data").joinpath("dataset").joinpath(filename)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_file(filepath, ext):
|
|
17
|
+
loaders = {
|
|
18
|
+
".csv": lambda f: pd.read_csv(f, index_col=0),
|
|
19
|
+
".xlsx": lambda f: pd.read_excel(f, index_col=0),
|
|
20
|
+
".parquet": pd.read_parquet,
|
|
21
|
+
".p": pd.read_parquet
|
|
22
|
+
}
|
|
23
|
+
if ext not in loaders:
|
|
24
|
+
raise ValueError(f"Unsupported file extension: {ext}")
|
|
25
|
+
|
|
26
|
+
return loaders[ext](filepath)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_datasets(files, continue_with_common_genes=False):
|
|
30
|
+
preprocessing = dload("config")["preprocessing"]
|
|
31
|
+
data_dict= {}
|
|
32
|
+
|
|
33
|
+
for filename, meta in files.items():
|
|
34
|
+
if isinstance(meta, pd.DataFrame):
|
|
35
|
+
df = meta
|
|
36
|
+
elif isinstance(meta, dict):
|
|
37
|
+
filepath = meta["path"]
|
|
38
|
+
if isinstance(filepath, pd.DataFrame):
|
|
39
|
+
df = filepath
|
|
40
|
+
else:
|
|
41
|
+
ext = os.path.splitext(filepath)[1]
|
|
42
|
+
df = _load_file(filepath, ext)
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"Unsupported data structure for '{filename}': {type(meta)}")
|
|
45
|
+
|
|
46
|
+
df.index = df.index.str.split().str[0]
|
|
47
|
+
if preprocessing.get('normalize'):
|
|
48
|
+
log.info(f"{filename}: Normalization.")
|
|
49
|
+
df = (df - df.mean()) / df.std(ddof=0)
|
|
50
|
+
|
|
51
|
+
if preprocessing.get('drop_na'):
|
|
52
|
+
log.info(f"{filename}: Dropping missing values.")
|
|
53
|
+
df = df.dropna(how="any")
|
|
54
|
+
|
|
55
|
+
if preprocessing.get('fill_na'):
|
|
56
|
+
log.info(f"{filename}: Filling missing values with column mean.")
|
|
57
|
+
#df = df.T.fillna(df.mean(axis=1)).T
|
|
58
|
+
df = data_imputation(df)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
data_dict[filename] = df
|
|
62
|
+
|
|
63
|
+
common_genes = get_common_genes(data_dict)
|
|
64
|
+
if continue_with_common_genes:
|
|
65
|
+
log.info(f"Continuing with common genes: {len(common_genes)}")
|
|
66
|
+
for filename, df in data_dict.items():
|
|
67
|
+
if df.index.isin(common_genes).any():
|
|
68
|
+
data_dict[filename] = df.loc[common_genes]
|
|
69
|
+
|
|
70
|
+
dsave({
|
|
71
|
+
"datasets": data_dict,
|
|
72
|
+
"sorting": {
|
|
73
|
+
k: v.get("sort", "high") if isinstance(v, dict) else "high"
|
|
74
|
+
for k, v in files.items()
|
|
75
|
+
}
|
|
76
|
+
}, "input")
|
|
77
|
+
log.done(f"Datasets loaded.")
|
|
78
|
+
return data_dict, common_genes
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def drop_bad_samples(df, max_na=0.1):
|
|
84
|
+
total_elements = df.shape[0] * df.shape[1]
|
|
85
|
+
percent_nan = np.isnan(df.values).sum() / total_elements if total_elements > 0 else 0
|
|
86
|
+
has_nan_per_sample = np.isnan(df.values).any(axis=0) # how many samples has NA.
|
|
87
|
+
|
|
88
|
+
log.info(f"Total: {total_elements}, Percent NaN: {percent_nan:.2%}, Samples with NaN: {np.sum(has_nan_per_sample)} / {df.shape[1]}")
|
|
89
|
+
|
|
90
|
+
num_genes = df.shape[0] # E.g., 1178 (total rows/genes)
|
|
91
|
+
na_per_sample = np.isnan(df.values).sum(axis=0) / num_genes # Fraction NA per sample (column)
|
|
92
|
+
good_samples = na_per_sample <= max_na # Keep if <=10% NA
|
|
93
|
+
data_filtered = df.loc[:, good_samples] # Drop bad samples (those >10% NA)
|
|
94
|
+
|
|
95
|
+
log.info(f"Filtered samples: {data_filtered.shape[1]} (removed {df.shape[1] - data_filtered.shape[1]} samples with >{max_na*100:.0f}% NAs)")
|
|
96
|
+
return data_filtered
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def data_imputation(df):
|
|
101
|
+
log.info("Imputing missing values with gene means ...")
|
|
102
|
+
gene_means = np.nanmean(df.values, axis=1) # 1D array: means per gene
|
|
103
|
+
data_values = df.values.copy()
|
|
104
|
+
rows, cols = np.where(np.isnan(data_values))
|
|
105
|
+
if len(rows) > 0:
|
|
106
|
+
data_values[rows, cols] = np.take(gene_means, rows)
|
|
107
|
+
|
|
108
|
+
df = pd.DataFrame(data_values, index=df.index, columns=df.columns)
|
|
109
|
+
log.info(f"Data after imputation: {df.shape[0]} genes, {df.shape[1]} samples")
|
|
110
|
+
return df
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_common_genes(datasets):
|
|
116
|
+
log.started("Finding common genes across datasets.")
|
|
117
|
+
gene_sets = [set(df.index) for df in datasets.values()]
|
|
118
|
+
common_genes = set.intersection(*gene_sets)
|
|
119
|
+
log.done(f"Common genes found: {len(common_genes)}")
|
|
120
|
+
dsave(common_genes, "common", "common_genes")
|
|
121
|
+
return list(common_genes)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def filter_matrix_by_genes(matrix, genes_present_in_terms):
|
|
125
|
+
log.started("Filtering matrix using genes present in terms.")
|
|
126
|
+
genes = matrix.index.intersection(genes_present_in_terms)
|
|
127
|
+
matrix = matrix.loc[genes, genes]
|
|
128
|
+
log.done(f"Filtering matrix: {matrix.shape}")
|
|
129
|
+
return matrix.loc[genes, genes]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def load_gold_standard():
|
|
135
|
+
|
|
136
|
+
config = dload("config")
|
|
137
|
+
common_genes = dload("common", "common_genes")
|
|
138
|
+
|
|
139
|
+
gold_standard_source = config['gold_standard']
|
|
140
|
+
log.started(f"Loading gold standard: {gold_standard_source}, Min complex size: {config['min_genes_in_complex']}, Jaccard filtering: {config['jaccard']}")
|
|
141
|
+
if not common_genes:
|
|
142
|
+
raise ValueError("Common genes not found.")
|
|
143
|
+
|
|
144
|
+
# Define gold standard file paths for predefined sources
|
|
145
|
+
gold_standard_files = {
|
|
146
|
+
"CORUM": "gold_standard/CORUM.parquet",
|
|
147
|
+
"GOBP": "gold_standard/GOBP.parquet",
|
|
148
|
+
"PATHWAY": "gold_standard/PATHWAY.parquet"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if gold_standard_source in gold_standard_files:
|
|
152
|
+
# Load predefined gold standard from package resources
|
|
153
|
+
filename = gold_standard_files[gold_standard_source]
|
|
154
|
+
filename_path = resources.files("pyflex.data").joinpath(filename)
|
|
155
|
+
if not filename_path.exists(): # Check if the file exists
|
|
156
|
+
raise ValueError(f"Invalid Gold Standard type: {gold_standard_source}. File not found.")
|
|
157
|
+
terms = pd.read_parquet(filename_path) # type: ignore
|
|
158
|
+
elif Path(gold_standard_source).suffix.lower() == '.csv':
|
|
159
|
+
# Load user-provided custom gold standard from CSV file
|
|
160
|
+
filename_path = Path(gold_standard_source)
|
|
161
|
+
if not filename_path.exists():
|
|
162
|
+
raise ValueError(f"Custom gold standard CSV file not found: {gold_standard_source}")
|
|
163
|
+
log.done(f"Loading custom gold standard from CSV: {gold_standard_source}")
|
|
164
|
+
terms = pd.read_csv(filename_path)
|
|
165
|
+
else:
|
|
166
|
+
raise ValueError(f"Invalid gold standard source: {gold_standard_source}. Must be one of {list(gold_standard_files.keys())} or a path to a .csv file.")
|
|
167
|
+
|
|
168
|
+
common_genes_set = set(common_genes)
|
|
169
|
+
terms["used_genes"] = terms["Genes"].apply(lambda x: list(set(x.split(";")) & common_genes_set))
|
|
170
|
+
terms["n_used_genes"] = terms["used_genes"].apply(len)
|
|
171
|
+
log.info(f"Applying min_genes_in_complex filtering: {config['min_genes_in_complex']}")
|
|
172
|
+
terms = terms[terms["n_used_genes"] >= config['min_genes_in_complex']]
|
|
173
|
+
terms["hash"] = terms["used_genes"].apply(lambda x: [hash(i) for i in x])
|
|
174
|
+
|
|
175
|
+
if config['jaccard']:
|
|
176
|
+
log.info("Applying Jaccard filtering. Remove terms with identical gene sets.")
|
|
177
|
+
terms = filter_duplicate_terms(terms)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
genes_present_in_terms = list(set(terms["used_genes"].explode().unique()) & common_genes_set)
|
|
181
|
+
# if there is column called "ID", set it as index
|
|
182
|
+
if "ID" in terms.columns:
|
|
183
|
+
terms = terms.set_index("ID")
|
|
184
|
+
|
|
185
|
+
dsave(terms, "common", "terms")
|
|
186
|
+
dsave(genes_present_in_terms, "common", "genes_present_in_terms")
|
|
187
|
+
log.done("Gold standard loading completed.")
|
|
188
|
+
return terms, genes_present_in_terms
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def filter_duplicate_terms(terms):
|
|
195
|
+
log.started("Filtering duplicate terms using optimized method.")
|
|
196
|
+
|
|
197
|
+
# Precompute frozen gene sets and hash them
|
|
198
|
+
terms = terms.copy()
|
|
199
|
+
terms["gene_set"] = terms["used_genes"].map(lambda x: frozenset(x))
|
|
200
|
+
|
|
201
|
+
# Group by identical gene sets
|
|
202
|
+
grouped = terms.groupby("gene_set", sort=False)
|
|
203
|
+
|
|
204
|
+
# Identify duplicate clusters (groups with >1 term)
|
|
205
|
+
duplicate_clusters = []
|
|
206
|
+
for _, group in grouped:
|
|
207
|
+
if len(group) > 1:
|
|
208
|
+
duplicate_clusters.append(group["ID"].values)
|
|
209
|
+
|
|
210
|
+
# Determine which IDs to keep (smallest ID in each duplicate cluster)
|
|
211
|
+
keep_ids = set(terms["ID"])
|
|
212
|
+
for cluster in duplicate_clusters:
|
|
213
|
+
sorted_ids = sorted(cluster)
|
|
214
|
+
keep_ids.difference_update(sorted_ids[1:]) # Remove all but smallest ID
|
|
215
|
+
|
|
216
|
+
# Filter and clean up
|
|
217
|
+
filtered = terms[terms["ID"].isin(keep_ids)].copy()
|
|
218
|
+
filtered.drop(columns=["gene_set"], inplace=True)
|
|
219
|
+
|
|
220
|
+
log.done(f"{len(terms) - len(filtered)} terms removed due to identical gene sets.")
|
|
221
|
+
return filtered
|
pythonflex/utils.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re # For sanitization (built-in, minimal regex)
|
|
3
|
+
import tempfile
|
|
4
|
+
import joblib
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
# Constants
|
|
9
|
+
TMP_ROOT = ".tmp"
|
|
10
|
+
VALID_EXTS = {".feather", ".npy", ".pkl"}
|
|
11
|
+
|
|
12
|
+
# Helper to sanitize names (make filesystem-safe)
|
|
13
|
+
def _sanitize(name):
|
|
14
|
+
if not name:
|
|
15
|
+
return "data"
|
|
16
|
+
# Replace forbidden/problematic chars with '_', collapse multiples, strip edges
|
|
17
|
+
safe = re.sub(r'[<>:"/\\|?*$,\s]+', '_', str(name).strip())
|
|
18
|
+
safe = re.sub(r'_+', '_', safe).strip('_')
|
|
19
|
+
return safe if safe else "data"
|
|
20
|
+
|
|
21
|
+
# Helper to get safe path
|
|
22
|
+
def _safe_path(category, name=None, ext=".pkl"):
|
|
23
|
+
safe_category = _sanitize(category)
|
|
24
|
+
dir_path = os.path.join(TMP_ROOT, safe_category)
|
|
25
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
26
|
+
safe_name = _sanitize(name) if name else "data"
|
|
27
|
+
return os.path.join(dir_path, f"{safe_name}{ext}")
|
|
28
|
+
|
|
29
|
+
# Save function
|
|
30
|
+
def dsave(data, category, name=None, path=None): # 'path' ignored for compatibility with old code
|
|
31
|
+
# If data is dict and no name, recurse on each item
|
|
32
|
+
if name is None and isinstance(data, dict):
|
|
33
|
+
for k, v in data.items():
|
|
34
|
+
dsave(v, category, k)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
# Choose best extension based on type
|
|
38
|
+
if isinstance(data, pd.DataFrame):
|
|
39
|
+
ext = ".feather"
|
|
40
|
+
save_func = lambda p: data.to_feather(p)
|
|
41
|
+
elif isinstance(data, np.ndarray):
|
|
42
|
+
ext = ".npy"
|
|
43
|
+
save_func = lambda p: np.save(p, data, allow_pickle=False)
|
|
44
|
+
else:
|
|
45
|
+
ext = ".pkl"
|
|
46
|
+
save_func = lambda p: joblib.dump(data, p, compress=0) # Add compress=3 if needed
|
|
47
|
+
|
|
48
|
+
target = _safe_path(category, name, ext)
|
|
49
|
+
|
|
50
|
+
# Atomic save: Write to temp file, then rename
|
|
51
|
+
with tempfile.NamedTemporaryFile(dir=os.path.dirname(target), delete=False) as tf:
|
|
52
|
+
tmp_path = tf.name
|
|
53
|
+
tf.close() # Close so save_func can write
|
|
54
|
+
save_func(tmp_path)
|
|
55
|
+
os.replace(tmp_path, target) # Atomic move
|
|
56
|
+
|
|
57
|
+
# Load function
|
|
58
|
+
def dload(category, name=None, path=None): # 'path' ignored for compatibility
|
|
59
|
+
dir_path = os.path.join(TMP_ROOT, _sanitize(category))
|
|
60
|
+
|
|
61
|
+
if not os.path.exists(dir_path):
|
|
62
|
+
return {}
|
|
63
|
+
|
|
64
|
+
if name is None:
|
|
65
|
+
# Load all in category as dict
|
|
66
|
+
out = {}
|
|
67
|
+
for filename in os.listdir(dir_path):
|
|
68
|
+
if not any(filename.endswith(ext) for ext in VALID_EXTS):
|
|
69
|
+
continue
|
|
70
|
+
k = os.path.splitext(filename)[0] # Key from filename (without ext)
|
|
71
|
+
full_path = os.path.join(dir_path, filename)
|
|
72
|
+
try:
|
|
73
|
+
if filename.endswith(".feather"):
|
|
74
|
+
out[k] = pd.read_feather(full_path)
|
|
75
|
+
elif filename.endswith(".npy"):
|
|
76
|
+
out[k] = np.load(full_path, mmap_mode="r") # MMap for perf
|
|
77
|
+
elif filename.endswith(".pkl"):
|
|
78
|
+
out[k] = joblib.load(full_path, mmap_mode="r") # MMap for perf
|
|
79
|
+
except (EOFError, ValueError, OSError):
|
|
80
|
+
print(f"Warning: '{full_path}' is corrupted. Skipping...")
|
|
81
|
+
os.remove(full_path) # Delete corrupted file
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
# Load specific name (try extensions in order)
|
|
85
|
+
for ext in VALID_EXTS:
|
|
86
|
+
target = _safe_path(category, name, ext)
|
|
87
|
+
if os.path.exists(target):
|
|
88
|
+
try:
|
|
89
|
+
if ext == ".feather":
|
|
90
|
+
return pd.read_feather(target)
|
|
91
|
+
elif ext == ".npy":
|
|
92
|
+
return np.load(target, mmap_mode="r") # MMap for perf
|
|
93
|
+
elif ext == ".pkl":
|
|
94
|
+
return joblib.load(target, mmap_mode="r") # MMap for perf
|
|
95
|
+
except (EOFError, ValueError, OSError):
|
|
96
|
+
print(f"Warning: '{target}' is corrupted. Deleting and returning {{}}...")
|
|
97
|
+
os.remove(target)
|
|
98
|
+
return {}
|
|
99
|
+
return {}
|
|
100
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonflex
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: pythonFLEX is a benchmarking toolkit for evaluating CRISPR screen results against biological gold standards. The toolkit computes gene-level and complex-level performance metrics, helping researchers systematically assess the biological relevance and resolution of their CRISPR screening data.
|
|
5
5
|
Author-email: Yasir Demirtaş <tyasird@hotmail.com>
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
pythonflex/__init__.py,sha256=rz-8y-zEJQR3ThbhlIUV0N1q7Z-4UNHpHbOglXFp52c,1292
|
|
2
|
+
pythonflex/analysis.py,sha256=Bu4wK7bnSlaXFRoqUjAGlQP5CoGYsrr1x_ZJRE1AuSc,52966
|
|
3
|
+
pythonflex/logging_config.py,sha256=QrwI9NSpi026rGKgitky_zP67aH6lo7rKA2TTAyrm1E,2011
|
|
4
|
+
pythonflex/plotting.py,sha256=ydC5CHIFl9GAGkYFyoXZ9v4ESKxTL_4vvoOkSR8hvCU,19508
|
|
5
|
+
pythonflex/preprocessing.py,sha256=wBuN3qfe7-qczzLtugOtnV3Un4QK4Kc_G8AZ1MMlhNk,8575
|
|
6
|
+
pythonflex/utils.py,sha256=nyVlGu5OXpz5YPj48hXueL5ja88sQ2PUiJ76c4USg4A,3886
|
|
7
|
+
pythonflex/data/dataset/liver_cell_lines_500_genes.csv,sha256=qfKsqPjL41Y1GuxxAhc-MfaNO0mX6Qju_SeynKSpEiM,238639
|
|
8
|
+
pythonflex/data/dataset/melanoma_cell_lines_500_genes.csv,sha256=ByxcaDDqLlRtAyuCKhHeFQCitIBk2-Q4Hn6k8BNUF6c,620887
|
|
9
|
+
pythonflex/data/dataset/neuroblastoma_cell_lines_500_genes.csv,sha256=IxJI8E-smagbxHRvTjvQLZxuu89MuCw8XrReMSsViUI,365992
|
|
10
|
+
pythonflex/data/gold_standard/CORUM.parquet,sha256=AkLiflQAeQ6K3HG-PIdLbZ8vEF9GtNObtlY7TkxHyaw,131858
|
|
11
|
+
pythonflex/data/gold_standard/GOBP.parquet,sha256=YQGhRcHSiN_cMKytCUYNCfcDwYj9L3TLFNwobiS2f3M,1025099
|
|
12
|
+
pythonflex/data/gold_standard/PATHWAY.parquet,sha256=bFRDe3PQ_TFc7B1uZuynwOGcgxESLaOy1Zt5gpQ1Oso,277386
|
|
13
|
+
pythonflex/data/gold_standard/corum.csv,sha256=2rZeyr2Ghm7f-gFxCZnhPtxI2jxRoiZMUEH2EJwAgsI,208889
|
|
14
|
+
pythonflex/data/gold_standard/gobp.csv,sha256=TO9yfx9mO8WkXvWfSB-pFId9T8xYfqdZpshAXC0Fyj8,1739167
|
|
15
|
+
pythonflex/data/gold_standard/pathway.csv,sha256=J3HKVLUZ_Oxucmn_14ieYp3Wr2lcKtp0nIl4_8_K2Yc,489424
|
|
16
|
+
pythonflex/examples/basic_usage.py,sha256=RBtJFphAE6NpCmSm9tMYN6FSdcpbWQzwkTlpX2ZejvI,2387
|
|
17
|
+
pythonflex/examples/dataset_filtering.py,sha256=YBHgkbHj9SpIj-qLxQhHS4UWjERv4MHyb2t_fR6AONE,971
|
|
18
|
+
pythonflex-0.1.3.dist-info/METADATA,sha256=U9S0qymqzZ0NUxAAy7Vjn9YXqRaSxdcSm3zI2wd-aoI,3928
|
|
19
|
+
pythonflex-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
20
|
+
pythonflex-0.1.3.dist-info/entry_points.txt,sha256=37liK1baI_CRVDivpjsn8JDClL9_YeTTuSMAZ3Ty7oE,47
|
|
21
|
+
pythonflex-0.1.3.dist-info/RECORD,,
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
pythonflex-0.1.2.dist-info/METADATA,sha256=LGYUDuQ3o_ANpHA6qovfk1w1VqNuz0pb-YwBs8tMNO0,3928
|
|
2
|
-
pythonflex-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
3
|
-
pythonflex-0.1.2.dist-info/entry_points.txt,sha256=37liK1baI_CRVDivpjsn8JDClL9_YeTTuSMAZ3Ty7oE,47
|
|
4
|
-
pythonflex-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|