sae-lens 6.6.5__py3-none-any.whl → 6.8.0__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.
sae_lens/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # ruff: noqa: E402
2
- __version__ = "6.6.5"
2
+ __version__ = "6.8.0"
3
3
 
4
4
  import logging
5
5
 
@@ -1,14 +1,10 @@
1
- import asyncio
2
1
  import json
3
- import os
4
2
  import urllib.parse
5
3
  import webbrowser
6
- from datetime import datetime
7
- from typing import Any, TypeVar
4
+ from typing import Any
8
5
 
9
6
  import requests
10
7
  from dotenv import load_dotenv
11
- from tenacity import retry, stop_after_attempt, wait_random_exponential
12
8
 
13
9
  from sae_lens import SAE, logger
14
10
 
@@ -126,47 +122,6 @@ class NeuronpediaFeature:
126
122
  return any(max(activation.act_values) > 0 for activation in self.activations)
127
123
 
128
124
 
129
- T = TypeVar("T")
130
-
131
-
132
- @retry(wait=wait_random_exponential(min=1, max=500), stop=stop_after_attempt(10))
133
- def sleep_identity(x: T) -> T:
134
- """Dummy function for retrying."""
135
- return x
136
-
137
-
138
- @retry(wait=wait_random_exponential(min=1, max=500), stop=stop_after_attempt(10))
139
- async def simulate_and_score( # type: ignore
140
- simulator: Any,
141
- activation_records: list[Any],
142
- ) -> Any:
143
- """Score an explanation of a neuron by how well it predicts activations on the given text sequences."""
144
- try:
145
- from neuron_explainer.explanations.scoring import (
146
- _simulate_and_score_sequence,
147
- aggregate_scored_sequence_simulations,
148
- )
149
- except ImportError as e:
150
- raise ImportError(
151
- "The neuron_explainer package is required to use this function. "
152
- "Please install SAELens with the neuronpedia optional dependencies: "
153
- "pip install sae-lens[neuronpedia]"
154
- ) from e
155
-
156
- scored_sequence_simulations = await asyncio.gather(
157
- *[
158
- sleep_identity(
159
- _simulate_and_score_sequence(
160
- simulator,
161
- activation_record,
162
- )
163
- )
164
- for activation_record in activation_records
165
- ]
166
- )
167
- return aggregate_scored_sequence_simulations(scored_sequence_simulations)
168
-
169
-
170
125
  def make_neuronpedia_list_with_features(
171
126
  api_key: str,
172
127
  list_name: str,
@@ -206,305 +161,3 @@ def test_key(api_key: str):
206
161
  response = requests.post(url, json=body)
207
162
  if response.status_code != 200:
208
163
  raise Exception("Neuronpedia API key is not valid.")
209
-
210
-
211
- async def autointerp_neuronpedia_features( # noqa: C901
212
- features: list[NeuronpediaFeature],
213
- openai_api_key: str | None = None,
214
- autointerp_retry_attempts: int = 3,
215
- autointerp_score_max_concurrent: int = 20,
216
- neuronpedia_api_key: str | None = None,
217
- skip_neuronpedia_api_key_test: bool = False,
218
- do_score: bool = True,
219
- output_dir: str = "neuronpedia_outputs/autointerp",
220
- num_activations_to_use: int = 20,
221
- max_explanation_activation_records: int = 20,
222
- upload_to_neuronpedia: bool = True,
223
- autointerp_explainer_model_name: str = "gpt-4-1106-preview",
224
- autointerp_scorer_model_name: str | None = "gpt-3.5-turbo",
225
- save_to_disk: bool = True,
226
- ):
227
- """
228
- Autointerp Neuronpedia features.
229
-
230
- Args:
231
- features: List of NeuronpediaFeature objects.
232
- openai_api_key: OpenAI API key.
233
- autointerp_retry_attempts: Number of retry attempts for autointerp.
234
- autointerp_score_max_concurrent: Maximum number of concurrent requests for autointerp scoring.
235
- neuronpedia_api_key: Neuronpedia API key.
236
- do_score: Whether to score the features.
237
- output_dir: Output directory for saving the results.
238
- num_activations_to_use: Number of activations to use.
239
- max_explanation_activation_records: Maximum number of activation records for explanation.
240
- upload_to_neuronpedia: Whether to upload the results to Neuronpedia.
241
- autointerp_explainer_model_name: Model name for autointerp explainer.
242
- autointerp_scorer_model_name: Model name for autointerp scorer.
243
-
244
- Returns:
245
- None
246
- """
247
- try:
248
- from neuron_explainer.activations.activation_records import (
249
- calculate_max_activation,
250
- )
251
- from neuron_explainer.activations.activations import ActivationRecord
252
- from neuron_explainer.explanations.calibrated_simulator import (
253
- UncalibratedNeuronSimulator,
254
- )
255
- from neuron_explainer.explanations.explainer import (
256
- HARMONY_V4_MODELS,
257
- ContextSize,
258
- TokenActivationPairExplainer,
259
- )
260
- from neuron_explainer.explanations.few_shot_examples import FewShotExampleSet
261
- from neuron_explainer.explanations.prompt_builder import PromptFormat
262
- from neuron_explainer.explanations.simulator import (
263
- LogprobFreeExplanationTokenSimulator,
264
- )
265
- except ImportError as e:
266
- raise ImportError(
267
- "The automated-interpretability package is required to use autointerp functionality. "
268
- "Please install SAELens with the neuronpedia optional dependencies: "
269
- "pip install sae-lens[neuronpedia]"
270
- ) from e
271
-
272
- logger.info("\n\n")
273
-
274
- if os.getenv("OPENAI_API_KEY") is None:
275
- if openai_api_key is None:
276
- raise Exception(
277
- "You need to provide an OpenAI API key either in environment variable OPENAI_API_KEY or as an argument."
278
- )
279
- os.environ["OPENAI_API_KEY"] = openai_api_key
280
-
281
- if autointerp_explainer_model_name not in HARMONY_V4_MODELS:
282
- raise Exception(
283
- f"Invalid explainer model name: {autointerp_explainer_model_name}. Must be one of: {HARMONY_V4_MODELS}"
284
- )
285
-
286
- if do_score and autointerp_scorer_model_name not in HARMONY_V4_MODELS:
287
- raise Exception(
288
- f"Invalid scorer model name: {autointerp_scorer_model_name}. Must be one of: {HARMONY_V4_MODELS}"
289
- )
290
-
291
- if upload_to_neuronpedia:
292
- if neuronpedia_api_key is None:
293
- raise Exception(
294
- "You need to provide a Neuronpedia API key to upload the results to Neuronpedia."
295
- )
296
- if not skip_neuronpedia_api_key_test:
297
- test_key(neuronpedia_api_key)
298
-
299
- logger.info("\n\n=== Step 1) Fetching features from Neuronpedia")
300
- for feature in features:
301
- feature_data = get_neuronpedia_feature(
302
- feature=feature.feature,
303
- layer=feature.layer,
304
- model=feature.modelId,
305
- dataset=feature.dataset,
306
- )
307
-
308
- if "modelId" not in feature_data:
309
- raise Exception(
310
- f"Feature {feature.feature} in layer {feature.layer} of model {feature.modelId} and dataset {feature.dataset} does not exist."
311
- )
312
-
313
- if "activations" not in feature_data or len(feature_data["activations"]) == 0:
314
- raise Exception(
315
- f"Feature {feature.feature} in layer {feature.layer} of model {feature.modelId} and dataset {feature.dataset} does not have activations."
316
- )
317
-
318
- activations = feature_data["activations"]
319
- activations_to_add = []
320
- for activation in activations:
321
- if len(activations_to_add) < num_activations_to_use:
322
- activations_to_add.append(
323
- NeuronpediaActivation(
324
- id=activation["id"],
325
- tokens=activation["tokens"],
326
- act_values=activation["values"],
327
- )
328
- )
329
- feature.activations = activations_to_add
330
-
331
- if not feature.has_activating_text():
332
- raise Exception(
333
- f"Feature {feature.modelId}@{feature.layer}-{feature.dataset}:{feature.feature} appears dead - it does not have activating text."
334
- )
335
-
336
- for iteration_num, feature in enumerate(features):
337
- start_time = datetime.now()
338
-
339
- logger.info(
340
- f"\n========== Feature {feature.modelId}@{feature.layer}-{feature.dataset}:{feature.feature} ({iteration_num + 1} of {len(features)} Features) =========="
341
- )
342
- logger.info(
343
- f"\n=== Step 2) Explaining feature {feature.modelId}@{feature.layer}-{feature.dataset}:{feature.feature}"
344
- )
345
-
346
- if feature.activations is None:
347
- feature.activations = []
348
- activation_records = [
349
- ActivationRecord(
350
- tokens=activation.tokens, # type: ignore
351
- activations=activation.act_values, # type: ignore
352
- ) # type: ignore
353
- for activation in feature.activations
354
- ]
355
-
356
- activation_records_explaining = activation_records[
357
- :max_explanation_activation_records
358
- ]
359
-
360
- explainer = TokenActivationPairExplainer(
361
- model_name=autointerp_explainer_model_name,
362
- prompt_format=PromptFormat.HARMONY_V4,
363
- context_size=ContextSize.SIXTEEN_K,
364
- max_concurrent=1,
365
- )
366
-
367
- explanations = []
368
- for _ in range(autointerp_retry_attempts):
369
- try:
370
- explanations = await explainer.generate_explanations(
371
- all_activation_records=activation_records_explaining,
372
- max_activation=calculate_max_activation(
373
- activation_records_explaining
374
- ),
375
- num_samples=1,
376
- )
377
- except Exception as e:
378
- logger.error(f"ERROR, RETRYING: {e}")
379
- else:
380
- break
381
- else:
382
- logger.error(
383
- f"ERROR: Failed to explain feature {feature.modelId}@{feature.layer}-{feature.dataset}:{feature.feature}"
384
- )
385
-
386
- if len(explanations) != 1:
387
- raise ValueError(
388
- f"Expected exactly one explanation but got {len(explanations)}. This may indicate an issue with the explainer's response."
389
- )
390
- explanation = explanations[0].rstrip(".")
391
- logger.info(
392
- f"===== {autointerp_explainer_model_name}'s explanation: {explanation}"
393
- )
394
- feature.autointerp_explanation = explanation
395
-
396
- scored_simulation = None
397
- if do_score and autointerp_scorer_model_name:
398
- logger.info(
399
- f"\n=== Step 3) Scoring feature {feature.modelId}@{feature.layer}-{feature.dataset}:{feature.feature}"
400
- )
401
- logger.info("=== This can take up to 30 seconds.")
402
-
403
- temp_activation_records = [
404
- ActivationRecord(
405
- tokens=[ # type: ignore
406
- token.replace("<|endoftext|>", "<|not_endoftext|>")
407
- .replace(" 55", "_55")
408
- .encode("ascii", errors="backslashreplace")
409
- .decode("ascii")
410
- for token in activation_record.tokens # type: ignore
411
- ],
412
- activations=activation_record.activations, # type: ignore
413
- ) # type: ignore
414
- for activation_record in activation_records
415
- ]
416
-
417
- score = None
418
- scored_simulation = None
419
- for _ in range(autointerp_retry_attempts):
420
- try:
421
- simulator = UncalibratedNeuronSimulator(
422
- LogprobFreeExplanationTokenSimulator(
423
- autointerp_scorer_model_name,
424
- explanation,
425
- json_mode=True,
426
- max_concurrent=autointerp_score_max_concurrent,
427
- few_shot_example_set=FewShotExampleSet.JL_FINE_TUNED,
428
- prompt_format=PromptFormat.HARMONY_V4,
429
- )
430
- )
431
- scored_simulation = await simulate_and_score(
432
- simulator, temp_activation_records
433
- )
434
- score = scored_simulation.get_preferred_score()
435
- except Exception as e:
436
- logger.error(f"ERROR, RETRYING: {e}")
437
- else:
438
- break
439
-
440
- if (
441
- score is None
442
- or scored_simulation is None
443
- or len(scored_simulation.scored_sequence_simulations)
444
- != num_activations_to_use
445
- ):
446
- logger.error(
447
- f"ERROR: Failed to score feature {feature.modelId}@{feature.layer}-{feature.dataset}:{feature.feature}. Skipping it."
448
- )
449
- continue
450
- feature.autointerp_explanation_score = score
451
- logger.info(
452
- f"===== {autointerp_scorer_model_name}'s score: {(score * 100):.0f}"
453
- )
454
-
455
- else:
456
- logger.info("=== Step 3) Skipping scoring as instructed.")
457
-
458
- feature_data = {
459
- "modelId": feature.modelId,
460
- "layer": f"{feature.layer}-{feature.dataset}",
461
- "index": feature.feature,
462
- "explanation": feature.autointerp_explanation,
463
- "explanationScore": feature.autointerp_explanation_score,
464
- "explanationModel": autointerp_explainer_model_name,
465
- }
466
- if do_score and autointerp_scorer_model_name and scored_simulation:
467
- feature_data["activations"] = feature.activations
468
- feature_data["simulationModel"] = autointerp_scorer_model_name
469
- feature_data["simulationActivations"] = (
470
- scored_simulation.scored_sequence_simulations
471
- ) # type: ignore
472
- feature_data["simulationScore"] = feature.autointerp_explanation_score
473
- feature_data_str = json.dumps(feature_data, default=vars)
474
-
475
- if save_to_disk:
476
- output_file = f"{output_dir}/{feature.modelId}-{feature.layer}-{feature.dataset}_feature-{feature.feature}_time-{datetime.now().strftime('%Y%m%d-%H%M%S')}.jsonl"
477
- os.makedirs(output_dir, exist_ok=True)
478
- logger.info(f"\n=== Step 4) Saving feature to {output_file}")
479
- with open(output_file, "a") as f:
480
- f.write(feature_data_str)
481
- f.write("\n")
482
- else:
483
- logger.info("\n=== Step 4) Skipping saving to disk.")
484
-
485
- if upload_to_neuronpedia:
486
- logger.info("\n=== Step 5) Uploading feature to Neuronpedia")
487
- upload_data = json.dumps(
488
- {
489
- "feature": feature_data,
490
- },
491
- default=vars,
492
- )
493
- upload_data_json = json.loads(upload_data, parse_constant=NanAndInfReplacer)
494
- url = f"{NEURONPEDIA_DOMAIN}/api/explanation/new"
495
- response = requests.post(
496
- url, json=upload_data_json, headers={"x-api-key": neuronpedia_api_key}
497
- )
498
- if response.status_code != 200:
499
- logger.error(
500
- f"ERROR: Couldn't upload explanation to Neuronpedia: {response.text}"
501
- )
502
- else:
503
- logger.info(
504
- f"===== Uploaded to Neuronpedia: {NEURONPEDIA_DOMAIN}/{feature.modelId}/{feature.layer}-{feature.dataset}/{feature.feature}"
505
- )
506
-
507
- end_time = datetime.now()
508
- logger.info(f"\n========== Time Spent for Feature: {end_time - start_time}\n")
509
-
510
- logger.info("\n\n========== Generation and Upload Complete ==========\n\n")
@@ -1252,11 +1252,11 @@ def get_mwhanna_transcoder_config_from_hf(
1252
1252
  try:
1253
1253
  # mwhanna transcoders sometimes have a typo in the config file name, so check for both
1254
1254
  wandb_config_path = hf_hub_download(
1255
- repo_id, "wanb-config.yaml", force_download=force_download
1255
+ repo_id, "wandb-config.yaml", force_download=force_download
1256
1256
  )
1257
1257
  except EntryNotFoundError:
1258
1258
  wandb_config_path = hf_hub_download(
1259
- repo_id, "wandb-config.yaml", force_download=force_download
1259
+ repo_id, "wanb-config.yaml", force_download=force_download
1260
1260
  )
1261
1261
  try:
1262
1262
  base_config_path = hf_hub_download(
@@ -1330,6 +1330,66 @@ def mwhanna_transcoder_huggingface_loader(
1330
1330
  return cfg_dict, state_dict, None
1331
1331
 
1332
1332
 
1333
+ def mntss_clt_layer_huggingface_loader(
1334
+ repo_id: str,
1335
+ folder_name: str,
1336
+ device: str = "cpu",
1337
+ force_download: bool = False, # noqa: ARG001
1338
+ cfg_overrides: dict[str, Any] | None = None,
1339
+ ) -> tuple[dict[str, Any], dict[str, torch.Tensor], torch.Tensor | None]:
1340
+ """
1341
+ Load a MNTSS CLT layer as a single layer transcoder.
1342
+ The assumption is that the `folder_name` is the layer to load as an int
1343
+ """
1344
+ base_config_path = hf_hub_download(
1345
+ repo_id, "config.yaml", force_download=force_download
1346
+ )
1347
+ with open(base_config_path) as f:
1348
+ cfg_info: dict[str, Any] = yaml.safe_load(f)
1349
+
1350
+ # We need to actually load the weights, since the config is missing most information
1351
+ encoder_path = hf_hub_download(
1352
+ repo_id,
1353
+ f"W_enc_{folder_name}.safetensors",
1354
+ force_download=force_download,
1355
+ )
1356
+ decoder_path = hf_hub_download(
1357
+ repo_id,
1358
+ f"W_dec_{folder_name}.safetensors",
1359
+ force_download=force_download,
1360
+ )
1361
+
1362
+ encoder_state_dict = load_file(encoder_path, device=device)
1363
+ decoder_state_dict = load_file(decoder_path, device=device)
1364
+
1365
+ with torch.no_grad():
1366
+ state_dict = {
1367
+ "W_enc": encoder_state_dict[f"W_enc_{folder_name}"].T, # type: ignore
1368
+ "b_enc": encoder_state_dict[f"b_enc_{folder_name}"], # type: ignore
1369
+ "b_dec": encoder_state_dict[f"b_dec_{folder_name}"], # type: ignore
1370
+ "W_dec": decoder_state_dict[f"W_dec_{folder_name}"].sum(dim=1), # type: ignore
1371
+ }
1372
+
1373
+ cfg_dict = {
1374
+ "architecture": "transcoder",
1375
+ "d_in": state_dict["b_dec"].shape[0],
1376
+ "d_out": state_dict["b_dec"].shape[0],
1377
+ "d_sae": state_dict["b_enc"].shape[0],
1378
+ "dtype": "float32",
1379
+ "device": device if device is not None else "cpu",
1380
+ "activation_fn": "relu",
1381
+ "normalize_activations": "none",
1382
+ "model_name": cfg_info["model_name"],
1383
+ "hook_name": f"blocks.{folder_name}.{cfg_info['feature_input_hook']}",
1384
+ "hook_name_out": f"blocks.{folder_name}.{cfg_info['feature_output_hook']}",
1385
+ "apply_b_dec_to_input": False,
1386
+ "model_from_pretrained_kwargs": {"fold_ln": False},
1387
+ **(cfg_overrides or {}),
1388
+ }
1389
+
1390
+ return cfg_dict, state_dict, None
1391
+
1392
+
1333
1393
  NAMED_PRETRAINED_SAE_LOADERS: dict[str, PretrainedSaeHuggingfaceLoader] = {
1334
1394
  "sae_lens": sae_lens_huggingface_loader,
1335
1395
  "connor_rob_hook_z": connor_rob_hook_z_huggingface_loader,
@@ -1341,6 +1401,7 @@ NAMED_PRETRAINED_SAE_LOADERS: dict[str, PretrainedSaeHuggingfaceLoader] = {
1341
1401
  "sparsify": sparsify_huggingface_loader,
1342
1402
  "gemma_2_transcoder": gemma_2_transcoder_huggingface_loader,
1343
1403
  "mwhanna_transcoder": mwhanna_transcoder_huggingface_loader,
1404
+ "mntss_clt_layer_transcoder": mntss_clt_layer_huggingface_loader,
1344
1405
  }
1345
1406
 
1346
1407
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sae-lens
3
- Version: 6.6.5
3
+ Version: 6.8.0
4
4
  Summary: Training and Analyzing Sparse Autoencoders (SAEs)
5
5
  License: MIT
6
6
  Keywords: deep-learning,sparse-autoencoders,mechanistic-interpretability,PyTorch
@@ -14,8 +14,6 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
16
  Provides-Extra: mamba
17
- Provides-Extra: neuronpedia
18
- Requires-Dist: automated-interpretability (>=0.0.5,<1.0.0) ; extra == "neuronpedia"
19
17
  Requires-Dist: babe (>=0.0.7,<0.0.8)
20
18
  Requires-Dist: datasets (>=3.1.0)
21
19
  Requires-Dist: mamba-lens (>=0.0.4,<0.0.5) ; extra == "mamba"
@@ -1,7 +1,7 @@
1
- sae_lens/__init__.py,sha256=gvg9photJRtatuXa9YF-uDv1tYiHwHTMh29X1GNQd6Y,3588
1
+ sae_lens/__init__.py,sha256=NKyGkQ37vjuyhYLswmZd6xGFcukOw7lKHFATjgFidD0,3588
2
2
  sae_lens/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  sae_lens/analysis/hooked_sae_transformer.py,sha256=vRu6JseH1lZaEeILD5bEkQEQ1wYHHDcxD-f2olKmE9Y,14275
4
- sae_lens/analysis/neuronpedia_integration.py,sha256=Fj4gVyaXMGBUxoK0vPeTwGVFr4n40fmfPrRENo4WzPs,19324
4
+ sae_lens/analysis/neuronpedia_integration.py,sha256=Gx1W7hUBEuMoasNcnOnZ1wmqbXDd1pSZ1nqKEya1HQc,4962
5
5
  sae_lens/cache_activations_runner.py,sha256=cNeAtp2JQ_vKbeddZVM-tcPLYyyfTWL8NDna5KQpkLI,12583
6
6
  sae_lens/config.py,sha256=IrjbsKBbaZoFXYrsPJ5xBwIqi9uZJIIFXjV_uoErJaE,28176
7
7
  sae_lens/constants.py,sha256=CSjmiZ-bhjQeVLyRvWxAjBokCgkfM8mnvd7-vxLIWTY,639
@@ -9,7 +9,7 @@ sae_lens/evals.py,sha256=4hanbyG8qZLItWqft94F4ZjUoytPVB7fw5s0P4Oi0VE,39504
9
9
  sae_lens/llm_sae_training_runner.py,sha256=exxNX_OEhdiUrlgmBP9bjX9DOf0HUcNQGO4unKeDjKM,13713
10
10
  sae_lens/load_model.py,sha256=C8AMykctj6H7tz_xRwB06-EXj6TfW64PtSJZR5Jxn1Y,8649
11
11
  sae_lens/loading/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- sae_lens/loading/pretrained_sae_loaders.py,sha256=tLeHArWFpu8CI6vXH1ZxFkhmsrhO2UsZyi7DzVzqAUs,44477
12
+ sae_lens/loading/pretrained_sae_loaders.py,sha256=IVtgxWN0w96ZORnWPYW2ndYWey7e5GpzlpedWF3NJ8k,46818
13
13
  sae_lens/loading/pretrained_saes_directory.py,sha256=4Vn-Jex6SveD7EbxcSOBv8cx1gkPfUMLU1QOP-ww1ZE,3752
14
14
  sae_lens/pretokenize_runner.py,sha256=w0f6SfZLAxbp5eAAKnet8RqUB_DKofZ9RGsoJwFnYbA,7058
15
15
  sae_lens/pretrained_saes.yaml,sha256=O_FwoOe7fU9_WLEOnMk1IWXRxD4nwzf1tCfbof1r0D0,598578
@@ -33,7 +33,7 @@ sae_lens/training/types.py,sha256=qSjmGzXf3MLalygG0psnVjmhX_mpLmL47MQtZfe7qxg,81
33
33
  sae_lens/training/upload_saes_to_huggingface.py,sha256=r_WzI1zLtGZ5TzAxuG3xa_8T09j3zXJrWd_vzPsPGkQ,4469
34
34
  sae_lens/tutorial/tsea.py,sha256=fd1am_XXsf2KMbByDapJo-2qlxduKaa62Z2qcQZ3QKU,18145
35
35
  sae_lens/util.py,sha256=mCwLAilGMVo8Scm7CIsCafU7GsfmBvCcjwmloI4Ly7Y,1718
36
- sae_lens-6.6.5.dist-info/LICENSE,sha256=DW6e-hDosiu4CfW0-imI57sV1I5f9UEslpviNQcOAKs,1069
37
- sae_lens-6.6.5.dist-info/METADATA,sha256=U5oP3RYgIE2EnHA2mwRImUcoyVBhYYwiRU199LM_R7c,5356
38
- sae_lens-6.6.5.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
39
- sae_lens-6.6.5.dist-info/RECORD,,
36
+ sae_lens-6.8.0.dist-info/LICENSE,sha256=DW6e-hDosiu4CfW0-imI57sV1I5f9UEslpviNQcOAKs,1069
37
+ sae_lens-6.8.0.dist-info/METADATA,sha256=DkYerQAH4AseU876uNcejPkZu4I3M2EscDlgG1NWwc0,5244
38
+ sae_lens-6.8.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
39
+ sae_lens-6.8.0.dist-info/RECORD,,