awc-helpers 0.1.1__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.
- awc_helpers/__init__.py +13 -1
- awc_helpers/awc_inference.py +28 -17
- awc_helpers/format_utils.py +149 -0
- {awc_helpers-0.1.1.dist-info → awc_helpers-0.1.3.dist-info}/METADATA +9 -8
- awc_helpers-0.1.3.dist-info/RECORD +9 -0
- awc_helpers-0.1.1.dist-info/RECORD +0 -8
- {awc_helpers-0.1.1.dist-info → awc_helpers-0.1.3.dist-info}/LICENSE +0 -0
- {awc_helpers-0.1.1.dist-info → awc_helpers-0.1.3.dist-info}/WHEEL +0 -0
- {awc_helpers-0.1.1.dist-info → awc_helpers-0.1.3.dist-info}/top_level.txt +0 -0
awc_helpers/__init__.py
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
"""AWC Helpers - Wildlife detection and classification inference tools."""
|
|
2
2
|
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
3
5
|
from .awc_inference import (
|
|
4
6
|
DetectAndClassify,
|
|
5
7
|
SpeciesClasInference,
|
|
6
8
|
format_md_detections,
|
|
7
9
|
load_classification_model,
|
|
8
10
|
)
|
|
11
|
+
from .format_utils import (
|
|
12
|
+
output_csv,
|
|
13
|
+
output_timelapse_json,
|
|
14
|
+
truncate_float,
|
|
15
|
+
truncate_float_array,
|
|
16
|
+
)
|
|
9
17
|
from .math_utils import crop_image, pil_to_tensor
|
|
10
18
|
|
|
11
|
-
__version__ = "
|
|
19
|
+
__version__ = version("awc_helpers")
|
|
12
20
|
|
|
13
21
|
__all__ = [
|
|
14
22
|
"DetectAndClassify",
|
|
@@ -17,4 +25,8 @@ __all__ = [
|
|
|
17
25
|
"load_classification_model",
|
|
18
26
|
"crop_image",
|
|
19
27
|
"pil_to_tensor",
|
|
28
|
+
"output_csv",
|
|
29
|
+
"output_timelapse_json",
|
|
30
|
+
"truncate_float",
|
|
31
|
+
"truncate_float_array",
|
|
20
32
|
]
|
awc_helpers/awc_inference.py
CHANGED
|
@@ -26,6 +26,7 @@ from megadetector.detection import run_detector
|
|
|
26
26
|
from typing import List, Tuple, Union
|
|
27
27
|
from PIL import Image
|
|
28
28
|
from .math_utils import crop_image, pil_to_tensor
|
|
29
|
+
from .format_utils import output_csv, output_timelapse_json
|
|
29
30
|
import logging
|
|
30
31
|
|
|
31
32
|
logger = logging.getLogger(__name__)
|
|
@@ -124,7 +125,6 @@ class SpeciesClasInference:
|
|
|
124
125
|
model: The loaded classification model.
|
|
125
126
|
label_names: List of class label names.
|
|
126
127
|
clas_threshold: Minimum confidence threshold for predictions.
|
|
127
|
-
pred_topn: Number of top predictions to return per image.
|
|
128
128
|
prob_round: Decimal places to round probabilities.
|
|
129
129
|
use_fp16: Whether to use FP16 mixed precision inference.
|
|
130
130
|
resize_size: Target size for image resizing before inference.
|
|
@@ -136,14 +136,13 @@ class SpeciesClasInference:
|
|
|
136
136
|
... classifier_base='tf_efficientnet_b5.ns_jft_in1k',
|
|
137
137
|
... label_names=['cat', 'dog', 'bird']
|
|
138
138
|
... )
|
|
139
|
-
>>> results = classifier.predict_batch([(image_path, bbox)])
|
|
139
|
+
>>> results = classifier.predict_batch([(image_path, bbox_conf,bbox)])
|
|
140
140
|
"""
|
|
141
141
|
|
|
142
142
|
def __init__(self,
|
|
143
143
|
classifier_path: str,
|
|
144
144
|
classifier_base: str,
|
|
145
145
|
label_names: List[str] = None,
|
|
146
|
-
pred_topn: int = 1,
|
|
147
146
|
prob_round: int = 4,
|
|
148
147
|
clas_threshold: float = 0.5,
|
|
149
148
|
resize_size: int = 300,
|
|
@@ -167,7 +166,6 @@ class SpeciesClasInference:
|
|
|
167
166
|
self.model.eval()
|
|
168
167
|
|
|
169
168
|
self.clas_threshold = clas_threshold
|
|
170
|
-
self.pred_topn=pred_topn
|
|
171
169
|
self.prob_round=prob_round
|
|
172
170
|
self.use_fp16=use_fp16 and self.device.type=='cuda'
|
|
173
171
|
self.resize_size=resize_size
|
|
@@ -197,7 +195,7 @@ class SpeciesClasInference:
|
|
|
197
195
|
img = source.convert('RGB') if source.mode != 'RGB' else source
|
|
198
196
|
return crop_image(img, bbox_norm, square_crop=True)
|
|
199
197
|
|
|
200
|
-
def _predict(self, input_tensor: torch.Tensor) -> torch.Tensor:
|
|
198
|
+
def _predict(self, input_tensor: torch.Tensor, pred_topn: int) -> torch.Tensor:
|
|
201
199
|
"""
|
|
202
200
|
Run classification model on input tensor.
|
|
203
201
|
|
|
@@ -219,7 +217,7 @@ class SpeciesClasInference:
|
|
|
219
217
|
# Softmax in fp32 for numerical stability
|
|
220
218
|
probs = torch.nn.functional.softmax(logits.float(), dim=1)
|
|
221
219
|
|
|
222
|
-
top_probs, top_indices = torch.topk(probs, k=
|
|
220
|
+
top_probs, top_indices = torch.topk(probs, k=pred_topn, dim=1)
|
|
223
221
|
return (top_probs.cpu().numpy().round(self.prob_round),
|
|
224
222
|
top_indices.cpu().numpy())
|
|
225
223
|
|
|
@@ -254,6 +252,7 @@ class SpeciesClasInference:
|
|
|
254
252
|
def predict_batch(
|
|
255
253
|
self,
|
|
256
254
|
inputs: List[Union[Tuple[str, float, Tuple[float, float, float, float]], Tuple[Image.Image, str, float, Tuple[float, float, float, float]]]],
|
|
255
|
+
pred_topn: int = 1,
|
|
257
256
|
batch_size: int = 1,
|
|
258
257
|
) -> List[Tuple]:
|
|
259
258
|
"""
|
|
@@ -262,7 +261,6 @@ class SpeciesClasInference:
|
|
|
262
261
|
Args:
|
|
263
262
|
inputs: List of (img_path, bbox_confidence, bbox) tuples, or (PIL Image, id, bbox_confidence, bbox) tuples for streaming
|
|
264
263
|
pred_topn: Number of top predictions to return
|
|
265
|
-
prob_round: Decimal places to round probabilities
|
|
266
264
|
batch_size: Number of images to process at once
|
|
267
265
|
|
|
268
266
|
Returns:
|
|
@@ -296,7 +294,7 @@ class SpeciesClasInference:
|
|
|
296
294
|
|
|
297
295
|
# Stack and run inference
|
|
298
296
|
batch_tensor = torch.cat(batch_tensors, dim=0)
|
|
299
|
-
top_probs, top_indices = self._predict(batch_tensor)
|
|
297
|
+
top_probs, top_indices = self._predict(batch_tensor, pred_topn=pred_topn)
|
|
300
298
|
|
|
301
299
|
for i, (identifier, bbox_conf, bbox) in enumerate(batch_metadata):
|
|
302
300
|
result = self._format_output(
|
|
@@ -337,7 +335,6 @@ class DetectAndClassify:
|
|
|
337
335
|
classifier_base: str = 'tf_efficientnet_b5.ns_jft_in1k',
|
|
338
336
|
detection_threshold: float = 0.1,
|
|
339
337
|
clas_threshold: float = 0.5,
|
|
340
|
-
pred_topn: int = 1,
|
|
341
338
|
resize_size: int = 300,
|
|
342
339
|
force_cpu: bool = False,
|
|
343
340
|
skip_clas_errors: bool = True):
|
|
@@ -351,7 +348,6 @@ class DetectAndClassify:
|
|
|
351
348
|
classifier_base: Name of the base timm model architecture.
|
|
352
349
|
detection_threshold: Minimum confidence for animal detections.
|
|
353
350
|
clas_threshold: Minimum confidence for classification predictions.
|
|
354
|
-
pred_topn: Number of top classification predictions to return.
|
|
355
351
|
resize_size: Target image size for classification model input.
|
|
356
352
|
force_cpu: If True, use CPU even if CUDA is available.
|
|
357
353
|
skip_clas_errors: If True, skip classification errors instead of raising.
|
|
@@ -362,7 +358,6 @@ class DetectAndClassify:
|
|
|
362
358
|
classifier_base=classifier_base,
|
|
363
359
|
clas_threshold=clas_threshold,
|
|
364
360
|
label_names=label_names,
|
|
365
|
-
pred_topn=pred_topn,
|
|
366
361
|
resize_size=resize_size,
|
|
367
362
|
force_cpu=force_cpu,
|
|
368
363
|
skip_errors=skip_clas_errors)
|
|
@@ -410,7 +405,9 @@ class DetectAndClassify:
|
|
|
410
405
|
self,
|
|
411
406
|
inp: Union[str, Image.Image, List[Union[str, Image.Image]]],
|
|
412
407
|
identifier: Union[str, List[str], None] = None,
|
|
413
|
-
clas_bs: int = 4
|
|
408
|
+
clas_bs: int = 4,
|
|
409
|
+
topn: int = 1,
|
|
410
|
+
output_name: str = None,
|
|
414
411
|
) -> List[Tuple]:
|
|
415
412
|
"""
|
|
416
413
|
Run detection and classification on input images.
|
|
@@ -424,11 +421,14 @@ class DetectAndClassify:
|
|
|
424
421
|
identifier: Optional identifier(s) for tracking results back to
|
|
425
422
|
source images. If None, uses file paths or timestamps.
|
|
426
423
|
clas_bs: Batch size for classification inference.
|
|
427
|
-
|
|
424
|
+
topn: Number of top classification predictions to return.
|
|
425
|
+
output_name: Optional name for saving results (CSV and Timelapse's JSON) instead of returning it.
|
|
428
426
|
Returns:
|
|
429
427
|
List of result tuples, one per detected animal. Each tuple contains:
|
|
430
|
-
(identifier, bbox, label1, prob1, label2, prob2, ...) where the
|
|
431
|
-
number of label/prob pairs depends on
|
|
428
|
+
(identifier, bbox_conf, bbox, label1, prob1, label2, prob2, ...) where the
|
|
429
|
+
number of label/prob pairs depends on topn and clas_threshold.
|
|
430
|
+
|
|
431
|
+
If output_name is provided, results are saved to file, no results returned.
|
|
432
432
|
"""
|
|
433
433
|
inp, identifier = self._validate_input(inp, identifier)
|
|
434
434
|
if len(inp) == 0:
|
|
@@ -438,7 +438,11 @@ class DetectAndClassify:
|
|
|
438
438
|
for item,id in zip(inp, identifier):
|
|
439
439
|
img = item
|
|
440
440
|
if isinstance(item,str):
|
|
441
|
-
|
|
441
|
+
try:
|
|
442
|
+
img = Image.open(item)
|
|
443
|
+
except Exception as e:
|
|
444
|
+
logger.warning(f"Failed to open image {item}: {e}")
|
|
445
|
+
continue
|
|
442
446
|
try:
|
|
443
447
|
md_result = self.md_detector.generate_detections_one_image(img,id,
|
|
444
448
|
detection_threshold=self.detection_threshold)
|
|
@@ -449,4 +453,11 @@ class DetectAndClassify:
|
|
|
449
453
|
if isinstance(item,str):
|
|
450
454
|
img.close()
|
|
451
455
|
|
|
452
|
-
|
|
456
|
+
clas_results = self.clas_inference.predict_batch(md_results, pred_topn=topn, batch_size=clas_bs)
|
|
457
|
+
if output_name is None:
|
|
458
|
+
return clas_results
|
|
459
|
+
|
|
460
|
+
output_csv(clas_results, output_name)
|
|
461
|
+
output_timelapse_json(clas_results, output_name, self.clas_inference.label_names)
|
|
462
|
+
|
|
463
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import json
|
|
3
|
+
import math
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import List, Tuple, Dict, Any
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def truncate_float(x: float, precision: int = 3) -> float:
|
|
10
|
+
"""
|
|
11
|
+
Truncates the fractional portion of a floating-point value to a specific number of
|
|
12
|
+
floating-point digits.
|
|
13
|
+
Source: https://github.com/agentmorris/MegaDetector/blob/main/megadetector/utils/ct_utils.py
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
x (float): scalar to truncate
|
|
17
|
+
precision (int, optional): the number of significant digits to preserve, should be >= 1
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
float: truncated version of [x]
|
|
21
|
+
"""
|
|
22
|
+
return math.floor(x * (10 ** precision)) / (10 ** precision)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def truncate_float_array(arr: List[float], precision: int = 4) -> List[float]:
|
|
26
|
+
return [truncate_float(x, precision) for x in arr]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def output_timelapse_json(clas_results: List[Tuple], json_name: str, label_names: List[str]):
|
|
30
|
+
"""
|
|
31
|
+
Convert classification results to timelapse JSON format.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
clas_results: List of result tuples, one per detected animal. Each tuple contains:
|
|
35
|
+
(identifier, bbox_conf, bbox, label1, prob1, label2, prob2, ...) where the
|
|
36
|
+
number of label/prob pairs depends on pred_topn and clas_threshold.
|
|
37
|
+
json_name: Output JSON file name.
|
|
38
|
+
label_names: List of all label names.
|
|
39
|
+
"""
|
|
40
|
+
if not json_name.endswith('.json'):
|
|
41
|
+
json_name += '.json'
|
|
42
|
+
|
|
43
|
+
# Group detections by file using OrderedDict to preserve order
|
|
44
|
+
images_dict: Dict[str, List[Dict[str, Any]]] = OrderedDict()
|
|
45
|
+
|
|
46
|
+
for result in clas_results:
|
|
47
|
+
identifier = result[0]
|
|
48
|
+
bbox_conf = result[1]
|
|
49
|
+
bbox = result[2]
|
|
50
|
+
|
|
51
|
+
# Initialize file entry if not exists
|
|
52
|
+
if identifier not in images_dict:
|
|
53
|
+
images_dict[identifier] = []
|
|
54
|
+
|
|
55
|
+
# If bbox is None or empty, this image has no detections
|
|
56
|
+
if bbox is None or bbox_conf is None:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
# Build detection object
|
|
60
|
+
detection = {
|
|
61
|
+
"category": "1", # Always "1" for animal
|
|
62
|
+
"conf": truncate_float(bbox_conf, precision=3),
|
|
63
|
+
"bbox": truncate_float_array(list(bbox), precision=4)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
clas2idx = {name: str(i + 1) for i, name in enumerate(label_names)}
|
|
67
|
+
|
|
68
|
+
classifications = []
|
|
69
|
+
for i in range(3, len(result), 2):
|
|
70
|
+
if i + 1 < len(result):
|
|
71
|
+
label_str = result[i]
|
|
72
|
+
prob = result[i + 1]
|
|
73
|
+
if label_str is not None and prob is not None:
|
|
74
|
+
classifications.append([clas2idx[label_str], truncate_float(prob, precision=3)])
|
|
75
|
+
|
|
76
|
+
if classifications:
|
|
77
|
+
detection["classifications"] = classifications
|
|
78
|
+
|
|
79
|
+
images_dict[identifier].append(detection)
|
|
80
|
+
|
|
81
|
+
# Build images list
|
|
82
|
+
images = []
|
|
83
|
+
for file_path, detections in images_dict.items():
|
|
84
|
+
images.append({
|
|
85
|
+
"file": file_path,
|
|
86
|
+
"detections": detections
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
idx2clas = {str(i + 1): name for i, name in enumerate(label_names)}
|
|
90
|
+
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
91
|
+
# Build output structure
|
|
92
|
+
output = {
|
|
93
|
+
"images": images,
|
|
94
|
+
"detection_categories": {
|
|
95
|
+
"1": "animal",
|
|
96
|
+
"2": "person",
|
|
97
|
+
"3": "vehicle"
|
|
98
|
+
},
|
|
99
|
+
"info": {
|
|
100
|
+
"detection_completion_time": current_time,
|
|
101
|
+
"format_version": "1.4",
|
|
102
|
+
"detector": "md_v1000.0.0-redwood.pt",
|
|
103
|
+
"detector_metadata": {
|
|
104
|
+
"megadetector_version": "1000-redwood"
|
|
105
|
+
},
|
|
106
|
+
"python_library": "awc-helpers"
|
|
107
|
+
},
|
|
108
|
+
"classification_categories": idx2clas
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Write to file
|
|
112
|
+
with open(json_name, 'w') as f:
|
|
113
|
+
json.dump(output, f, indent=1)
|
|
114
|
+
|
|
115
|
+
def output_csv(clas_results: List[Tuple],csv_name: str):
|
|
116
|
+
"""
|
|
117
|
+
Convert classification results to CSV format.
|
|
118
|
+
Args:
|
|
119
|
+
clas_results: List of result tuples, one per detected animal. Each tuple contains:
|
|
120
|
+
(identifier, bbox_conf, bbox, label1, prob1, label2, prob2, ...) where the
|
|
121
|
+
number of label/prob pairs depends on pred_topn and clas_threshold.
|
|
122
|
+
csv_name: Output CSV file name.
|
|
123
|
+
"""
|
|
124
|
+
if not csv_name.endswith('.csv'):
|
|
125
|
+
csv_name += '.csv'
|
|
126
|
+
|
|
127
|
+
# Determine the maximum number of label/prob pairs
|
|
128
|
+
max_pairs = 0
|
|
129
|
+
for result in clas_results:
|
|
130
|
+
num_pairs = (len(result) - 3) // 2
|
|
131
|
+
if num_pairs > max_pairs:
|
|
132
|
+
max_pairs = num_pairs
|
|
133
|
+
|
|
134
|
+
# Create CSV header
|
|
135
|
+
header = ['Image Path', 'Bounding Box Confidence', 'Bounding Box Normalized']
|
|
136
|
+
for i in range(1, max_pairs + 1):
|
|
137
|
+
header.append(f'Label {i}')
|
|
138
|
+
header.append(f'Confidence {i}')
|
|
139
|
+
|
|
140
|
+
# Write to CSV
|
|
141
|
+
with open(csv_name, mode='w', newline='') as csv_file:
|
|
142
|
+
writer = csv.writer(csv_file)
|
|
143
|
+
writer.writerow(header)
|
|
144
|
+
for result in clas_results:
|
|
145
|
+
row = list(result)
|
|
146
|
+
# Pad the row with empty strings if necessary
|
|
147
|
+
while len(row) < 3 + 2 * max_pairs:
|
|
148
|
+
row.append('')
|
|
149
|
+
writer.writerow(row)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: awc-helpers
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Australian Wildlife Conservancy's Wildlife detection and species classification inference tools
|
|
5
5
|
Author: Quan Tran
|
|
6
6
|
License: CC-BY-NC-SA-4.0
|
|
@@ -51,11 +51,6 @@ pip install torch==2.9.1
|
|
|
51
51
|
pip install awc-helpers
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
**From GitHub:**
|
|
55
|
-
```bash
|
|
56
|
-
pip install git+https://github.com/Australian-Wildlife-Conservancy-AWC/awc_inference.git
|
|
57
|
-
```
|
|
58
|
-
|
|
59
54
|
## Usage
|
|
60
55
|
|
|
61
56
|
```python
|
|
@@ -72,13 +67,19 @@ pipeline = DetectAndClassify(
|
|
|
72
67
|
|
|
73
68
|
# Run inference on image paths
|
|
74
69
|
results = pipeline.predict(
|
|
75
|
-
inp=["image1.jpg", "image2.jpg"],
|
|
70
|
+
inp=["path/to/image1.jpg", "path/to/image2.jpg"],
|
|
76
71
|
clas_bs=4
|
|
77
72
|
)
|
|
78
73
|
|
|
79
|
-
# Results format: [(
|
|
74
|
+
# Results format: [(image_path, bbox_confidence, bbox, label, label_confidence), ...]
|
|
80
75
|
for result in results:
|
|
81
76
|
print(result)
|
|
77
|
+
# print example:
|
|
78
|
+
# ("path/to/image1.jpg",
|
|
79
|
+
# 0.804,
|
|
80
|
+
# (0.2246, 0.5885, 0.0678, 0.1022),
|
|
81
|
+
# 'Acanthagenys rufogularis | Spiny-cheeked Honeyeater',
|
|
82
|
+
# 0.9948)
|
|
82
83
|
```
|
|
83
84
|
|
|
84
85
|
## License
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
awc_helpers/__init__.py,sha256=0jyHX2tDNfTsmMXJzFXbVXzDuSczir6987zz5dW529w,757
|
|
2
|
+
awc_helpers/awc_inference.py,sha256=w409rll5Ftnuxt9P5TdwvrxCMd-5pq9YgNRHgptBPQw,20036
|
|
3
|
+
awc_helpers/format_utils.py,sha256=rk0ibOxhW8zqihe-0jHo5FUMRsiDUNWBtKGXLBXN1XQ,5292
|
|
4
|
+
awc_helpers/math_utils.py,sha256=W3G5PGVkiMLM31qjsdJhmJq6KY46XZr6rj1SXWjK4Gk,2656
|
|
5
|
+
awc_helpers-0.1.3.dist-info/LICENSE,sha256=-K8JM-Ym5RhIZI1Wh5soC8hkvOhgA4R0fNpbpucPizg,1659
|
|
6
|
+
awc_helpers-0.1.3.dist-info/METADATA,sha256=l7-qS7oMGFuIDgos1wD_zn6uW1uP9jGXjUbt4NvU7nE,2789
|
|
7
|
+
awc_helpers-0.1.3.dist-info/WHEEL,sha256=hPN0AlP2dZM_3ZJZWP4WooepkmU9wzjGgCLCeFjkHLA,92
|
|
8
|
+
awc_helpers-0.1.3.dist-info/top_level.txt,sha256=_Xvw_DTZwJ6szEkcXxA1_AGYwKN-Uj6jfz0bF07-S8M,12
|
|
9
|
+
awc_helpers-0.1.3.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
awc_helpers/__init__.py,sha256=xStt2_-5rK5MvKtbFjMr6w2GqibT7EZWg7SgXmJ_gS8,474
|
|
2
|
-
awc_helpers/awc_inference.py,sha256=x3Lrj7txQ5oaz7rpU_6aKoCIJYcFVyulUH7AMDBDIus,19533
|
|
3
|
-
awc_helpers/math_utils.py,sha256=W3G5PGVkiMLM31qjsdJhmJq6KY46XZr6rj1SXWjK4Gk,2656
|
|
4
|
-
awc_helpers-0.1.1.dist-info/LICENSE,sha256=-K8JM-Ym5RhIZI1Wh5soC8hkvOhgA4R0fNpbpucPizg,1659
|
|
5
|
-
awc_helpers-0.1.1.dist-info/METADATA,sha256=t775UXQUnRNco6chjA8mQ50YcqRoupB-9gwMVxHEyoc,2710
|
|
6
|
-
awc_helpers-0.1.1.dist-info/WHEEL,sha256=hPN0AlP2dZM_3ZJZWP4WooepkmU9wzjGgCLCeFjkHLA,92
|
|
7
|
-
awc_helpers-0.1.1.dist-info/top_level.txt,sha256=_Xvw_DTZwJ6szEkcXxA1_AGYwKN-Uj6jfz0bF07-S8M,12
|
|
8
|
-
awc_helpers-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|