citrascope 0.4.0__py3-none-any.whl → 0.5.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.
@@ -10,26 +10,6 @@ const DEFAULT_API_PORT = 443;
10
10
  let currentAdapterSchema = [];
11
11
  export let currentConfig = {};
12
12
 
13
- /**
14
- * Initialize configuration management
15
- */
16
- async function fetchVersion() {
17
- try {
18
- const response = await fetch('/api/version');
19
- const data = await response.json();
20
- const versionEl = document.getElementById('citraScopeVersion');
21
- if (versionEl && data.version) {
22
- versionEl.textContent = data.version;
23
- }
24
- } catch (error) {
25
- console.error('Error fetching version:', error);
26
- const versionEl = document.getElementById('citraScopeVersion');
27
- if (versionEl) {
28
- versionEl.textContent = 'unknown';
29
- }
30
- }
31
- }
32
-
33
13
  export async function initConfig() {
34
14
  // Populate hardware adapter dropdown
35
15
  await loadAdapterOptions();
@@ -69,7 +49,6 @@ export async function initConfig() {
69
49
  // Load initial config
70
50
  await loadConfiguration();
71
51
  checkConfigStatus();
72
- fetchVersion();
73
52
  }
74
53
 
75
54
  /**
@@ -372,9 +351,23 @@ async function saveConfiguration(event) {
372
351
  const result = await saveConfig(config);
373
352
 
374
353
  if (result.ok) {
375
- showConfigSuccess(result.data.message || 'Configuration saved and applied successfully!');
354
+ // After config saved successfully, save any modified filter focus positions
355
+ const filterResults = await saveModifiedFilters();
356
+
357
+ // Build success message based on results
358
+ let message = result.data.message || 'Configuration saved and applied successfully!';
359
+ if (filterResults.success > 0) {
360
+ message += ` Updated ${filterResults.success} filter focus position${filterResults.success > 1 ? 's' : ''}.`;
361
+ }
362
+ if (filterResults.failed > 0) {
363
+ message += ` Warning: ${filterResults.failed} filter update${filterResults.failed > 1 ? 's' : ''} failed.`;
364
+ }
365
+
366
+ showConfigSuccess(message);
376
367
  } else {
377
- showConfigError(result.data.error || result.data.message || 'Failed to save configuration');
368
+ // Check for specific error codes
369
+ const errorMsg = result.data.error || result.data.message || 'Failed to save configuration';
370
+ showConfigError(errorMsg);
378
371
  }
379
372
  } catch (error) {
380
373
  showConfigError('Failed to save configuration: ' + error.message);
@@ -434,5 +427,175 @@ export function showConfigSection() {
434
427
  }
435
428
  }
436
429
 
430
+ /**
431
+ * Load and display filter configuration
432
+ */
433
+ async function loadFilterConfig() {
434
+ const filterSection = document.getElementById('filterConfigSection');
435
+
436
+ try {
437
+ const response = await fetch('/api/adapter/filters');
438
+
439
+ if (response.status === 404 || response.status === 503) {
440
+ // Adapter doesn't support filters or isn't available
441
+ if (filterSection) filterSection.style.display = 'none';
442
+ return;
443
+ }
444
+
445
+ const data = await response.json();
446
+
447
+ if (response.ok && data.filters) {
448
+ // Show the filter section
449
+ if (filterSection) filterSection.style.display = 'block';
450
+
451
+ // Populate filter table
452
+ const tbody = document.getElementById('filterTableBody');
453
+ const noFiltersMsg = document.getElementById('noFiltersMessage');
454
+
455
+ if (tbody) {
456
+ tbody.innerHTML = '';
457
+ const filters = data.filters;
458
+ const filterIds = Object.keys(filters).sort();
459
+
460
+ if (filterIds.length === 0) {
461
+ if (noFiltersMsg) noFiltersMsg.style.display = 'block';
462
+ } else {
463
+ if (noFiltersMsg) noFiltersMsg.style.display = 'none';
464
+
465
+ filterIds.forEach(filterId => {
466
+ const filter = filters[filterId];
467
+ const row = document.createElement('tr');
468
+ row.innerHTML = `
469
+ <td>${filterId}</td>
470
+ <td>${filter.name}</td>
471
+ <td>
472
+ <input type="number"
473
+ class="form-control form-control-sm filter-focus-input"
474
+ data-filter-id="${filterId}"
475
+ value="${filter.focus_position}"
476
+ min="0"
477
+ step="1">
478
+ </td>
479
+ `;
480
+ tbody.appendChild(row);
481
+ });
482
+ }
483
+ }
484
+ } else {
485
+ if (filterSection) filterSection.style.display = 'none';
486
+ }
487
+ } catch (error) {
488
+ console.error('Error loading filter config:', error);
489
+ if (filterSection) filterSection.style.display = 'none';
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Save all filter focus positions (called during main config save)
495
+ * Returns: Object with { success: number, failed: number }
496
+ */
497
+ async function saveModifiedFilters() {
498
+ const inputs = document.querySelectorAll('.filter-focus-input');
499
+ if (inputs.length === 0) return { success: 0, failed: 0 }; // No filters to save
500
+
501
+ let successCount = 0;
502
+ let failedCount = 0;
503
+
504
+ // Save all filter values
505
+ for (const input of inputs) {
506
+ const filterId = input.dataset.filterId;
507
+ const focusPosition = parseInt(input.value);
508
+
509
+ if (isNaN(focusPosition) || focusPosition < 0) {
510
+ failedCount++;
511
+ continue;
512
+ }
513
+
514
+ try {
515
+ const response = await fetch(`/api/adapter/filters/${filterId}`, {
516
+ method: 'PATCH',
517
+ headers: {
518
+ 'Content-Type': 'application/json'
519
+ },
520
+ body: JSON.stringify({ focus_position: focusPosition })
521
+ });
522
+
523
+ if (response.ok) {
524
+ successCount++;
525
+ } else {
526
+ failedCount++;
527
+ console.error(`Failed to save filter ${filterId}: HTTP ${response.status}`);
528
+ }
529
+ } catch (error) {
530
+ failedCount++;
531
+ console.error(`Error saving filter ${filterId}:`, error);
532
+ }
533
+ }
534
+
535
+ return { success: successCount, failed: failedCount };
536
+ }
537
+
538
+ /**
539
+ * Trigger autofocus routine
540
+ */
541
+ async function triggerAutofocus() {
542
+ const button = document.getElementById('runAutofocusButton');
543
+ const buttonText = document.getElementById('autofocusButtonText');
544
+ const buttonSpinner = document.getElementById('autofocusButtonSpinner');
545
+
546
+ if (!button || !buttonText || !buttonSpinner) return;
547
+
548
+ // Clear any previous messages
549
+ hideConfigMessages();
550
+
551
+ // Disable button and show spinner
552
+ button.disabled = true;
553
+ buttonText.textContent = 'Running Autofocus...';
554
+ buttonSpinner.style.display = 'inline-block';
555
+
556
+ try {
557
+ const response = await fetch('/api/adapter/autofocus', {
558
+ method: 'POST'
559
+ });
560
+
561
+ const data = await response.json();
562
+
563
+ if (response.ok) {
564
+ showConfigSuccess('Autofocus completed successfully');
565
+ // Reload filter config to show updated focus positions
566
+ await loadFilterConfig();
567
+ } else {
568
+ // Show clear error message (e.g., if autofocus already running or other conflict)
569
+ showConfigError(data.error || 'Autofocus failed');
570
+ }
571
+ } catch (error) {
572
+ console.error('Error triggering autofocus:', error);
573
+ showConfigError('Failed to trigger autofocus');
574
+ } finally {
575
+ // Re-enable button
576
+ button.disabled = false;
577
+ buttonText.textContent = 'Run Autofocus';
578
+ buttonSpinner.style.display = 'none';
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Initialize filter configuration on page load
584
+ */
585
+ export async function initFilterConfig() {
586
+ // Load filter config when config section is visible
587
+ await loadFilterConfig();
588
+ }
589
+
590
+ /**
591
+ * Setup autofocus button event listener (call once during init)
592
+ */
593
+ export function setupAutofocusButton() {
594
+ const autofocusBtn = document.getElementById('runAutofocusButton');
595
+ if (autofocusBtn) {
596
+ autofocusBtn.addEventListener('click', triggerAutofocus);
597
+ }
598
+ }
599
+
437
600
  // Make showConfigSection available globally for onclick handlers in HTML
438
601
  window.showConfigSection = showConfigSection;
@@ -20,13 +20,13 @@
20
20
  <div class="container">
21
21
  <header class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom align-items-center">
22
22
  <span class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none">
23
- <span class="fs-4 d-flex align-items-center gap-2">
23
+ <span class="fs-4 d-flex align-items-baseline gap-2">
24
24
  <img src="/static/img/citra.png" alt="CitraScope Logo" class="logo-img" />
25
25
  CitraScope
26
-
27
- </span>
28
- <span class="ms-2 me-3 d-flex align-items-center status-badge-container">
29
- <span id="wsStatus"><span class="badge rounded-pill bg-secondary" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Dashboard connection status">Connecting...</span></span>
26
+ <small class="text-muted" style="font-size: 0.875rem;">
27
+ <span id="headerVersion" style="cursor: pointer; user-select: none;" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Click to check for updates">v...</span>
28
+ <span id="updateIndicator" class="badge bg-info text-dark ms-1" style="display: none; font-size: 0.7rem;"></span>
29
+ </small>
30
30
  </span>
31
31
  </span>
32
32
 
@@ -58,8 +58,8 @@
58
58
  </div>
59
59
  <div class="card-body">
60
60
  <div class="row mb-2">
61
- <div class="col-6 fw-semibold">Hardware</div>
62
- <div class="col-6" id="hardwareAdapter">-</div>
61
+ <div class="col-6 fw-semibold">Daemon</div>
62
+ <div class="col-6" id="wsStatus"><span class="badge rounded-pill bg-secondary" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Dashboard connection status">Connecting...</span></div>
63
63
  </div>
64
64
  <div class="row mb-2">
65
65
  <div class="col-6 fw-semibold">Telescope</div>
@@ -69,6 +69,15 @@
69
69
  <div class="col-6 fw-semibold">Camera</div>
70
70
  <div class="col-6" id="cameraConnected"><span class="badge rounded-pill bg-secondary">Unknown</span></div>
71
71
  </div>
72
+ <div class="row">
73
+ <div class="col-6 fw-semibold">Task Processing</div>
74
+ <div class="col-6 d-flex align-items-center gap-2">
75
+ <span id="processingStatus"><span class="badge rounded-pill bg-success">Active</span></span>
76
+ <button id="toggleProcessingButton" class="btn btn-sm btn-secondary" title="Pause task processing">
77
+ <span id="processingButtonIcon">Pause</span>
78
+ </button>
79
+ </div>
80
+ </div>
72
81
  </div>
73
82
  </div>
74
83
  </div>
@@ -78,6 +87,10 @@
78
87
  Telescope
79
88
  </div>
80
89
  <div class="card-body">
90
+ <div class="row mb-2">
91
+ <div class="col-6 fw-semibold">Adapter</div>
92
+ <div class="col-6" id="hardwareAdapter">-</div>
93
+ </div>
81
94
  <div class="row mb-2">
82
95
  <div class="col-6 fw-semibold">Right Ascension</div>
83
96
  <div class="col-6" id="telescopeRA">-</div>
@@ -225,6 +238,38 @@
225
238
 
226
239
  <!-- Dynamic Adapter Settings Container -->
227
240
  <div id="adapter-settings-container"></div>
241
+
242
+ <!-- Filter Configuration Section (shown when adapter supports filters) -->
243
+ <div id="filterConfigSection" style="display: none; margin-top: 1.5rem;">
244
+ <hr class="border-secondary">
245
+ <div class="d-flex justify-content-between align-items-center mb-3">
246
+ <h5 class="mb-0">Filter Configuration</h5>
247
+ <div class="text-end">
248
+ <button type="button" class="btn btn-sm btn-outline-primary" id="runAutofocusButton">
249
+ <span id="autofocusButtonText">Run Autofocus</span>
250
+ <span id="autofocusButtonSpinner" class="spinner-border spinner-border-sm ms-2" style="display: none;" role="status"></span>
251
+ </button>
252
+ <div><small class="text-muted">Note: Pause task processing before running autofocus</small></div>
253
+ </div>
254
+ </div>
255
+ <div id="filterTableContainer">
256
+ <table class="table table-dark table-sm">
257
+ <thead>
258
+ <tr>
259
+ <th>Filter ID</th>
260
+ <th>Name</th>
261
+ <th>Focus Position</th>
262
+ </tr>
263
+ </thead>
264
+ <tbody id="filterTableBody">
265
+ <!-- Filter rows will be populated by JavaScript -->
266
+ </tbody>
267
+ </table>
268
+ <div id="noFiltersMessage" class="text-muted small" style="display: none;">
269
+ No filters configured. Connect to hardware to discover filters.
270
+ </div>
271
+ </div>
272
+ </div>
228
273
  </div>
229
274
  </div>
230
275
  </div>
@@ -304,15 +349,6 @@
304
349
  </small>
305
350
  </div>
306
351
  </div>
307
-
308
- <!-- Version Info -->
309
- <div class="row mt-3">
310
- <div class="col">
311
- <small class="text-muted">
312
- CitraScope Version: <span id="citraScopeVersion" class="text-secondary">Loading...</span>
313
- </small>
314
- </div>
315
- </div>
316
352
  </form>
317
353
  </div>
318
354
 
@@ -399,6 +435,53 @@
399
435
  </div>
400
436
 
401
437
 
438
+ <!-- Version Check Modal -->
439
+ <div class="modal fade" id="versionModal" tabindex="-1" aria-labelledby="versionModalLabel" aria-hidden="true">
440
+ <div class="modal-dialog modal-dialog-centered">
441
+ <div class="modal-content bg-dark text-light border-secondary">
442
+ <div class="modal-header border-secondary">
443
+ <h5 class="modal-title" id="versionModalLabel">Version Check</h5>
444
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
445
+ </div>
446
+ <div class="modal-body">
447
+ <div id="versionCheckLoading" style="text-align: center;">
448
+ <div class="spinner-border text-primary" role="status">
449
+ <span class="visually-hidden">Checking...</span>
450
+ </div>
451
+ <p class="mt-2">Checking for updates...</p>
452
+ </div>
453
+ <div id="versionCheckUpToDate" style="display: none;">
454
+ <p class="text-success">✓ You're up to date!</p>
455
+ <p>
456
+ <strong>Current version:</strong> <span id="modalCurrentVersionUpToDate">-</span>
457
+ </p>
458
+ </div>
459
+ <div id="versionCheckUpdateAvailable" style="display: none;">
460
+ <p class="text-warning">A new version of CitraScope is available!</p>
461
+ <p class="mb-3">
462
+ <strong>Current:</strong> <span id="modalCurrentVersion">-</span><br>
463
+ <strong>Latest:</strong> <span id="modalLatestVersion">-</span>
464
+ </p>
465
+ <p class="mb-2">To upgrade, run:</p>
466
+ <pre class="bg-secondary p-2 rounded"><code>pip install -U citrascope</code></pre>
467
+ <p class="mb-0">
468
+ <a href="#" id="releaseNotesLink" target="_blank" class="text-info">View release notes on GitHub →</a>
469
+ </p>
470
+ </div>
471
+ <div id="versionCheckError" style="display: none;">
472
+ <p class="text-muted">Unable to check for updates. You may be offline or GitHub is unreachable.</p>
473
+ <p>
474
+ <strong>Current version:</strong> <span id="modalCurrentVersionError">-</span>
475
+ </p>
476
+ </div>
477
+ </div>
478
+ <div class="modal-footer border-secondary">
479
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
480
+ </div>
481
+ </div>
482
+ </div>
483
+ </div>
484
+
402
485
  <script type="module" src="/static/app.js"></script>
403
486
 
404
487
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: citrascope
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Remotely control a telescope while it polls for tasks, collects and edge processes data, and delivers results and data for further processing.
5
5
  Author-email: Patrick McDavid <patrick@citra.space>
6
6
  License-Expression: MIT
@@ -1,38 +1,38 @@
1
1
  citrascope/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  citrascope/__main__.py,sha256=W7uY30MOuIlmhzVEoiZ_6BMmKrsqIvxcAxqw4-pOT3k,596
3
- citrascope/citra_scope_daemon.py,sha256=mw7U2gEgfDc3TuXVUHZAkHjSwY2T259FW02LAjz7vWU,10315
3
+ citrascope/citra_scope_daemon.py,sha256=XPskPbtenx-WNenlnbb5QhvvH_amK4vST-Uw6Pg2cts,13567
4
4
  citrascope/constants.py,sha256=Mc7SLzCelUMDV96BIwP684fFLGANCOEO_mM3GCeRDVY,968
5
5
  citrascope/api/abstract_api_client.py,sha256=gjmA9mw1O-TK16nYahOWClAwqPc_L1E3F2llZJeKTPw,624
6
6
  citrascope/api/citra_api_client.py,sha256=8rpz25Diy8YhuCiQ9HqMi4TIqxAc6BbrvqoFu8u-orQ,6007
7
- citrascope/hardware/abstract_astro_hardware_adapter.py,sha256=BdQrZkLSh2siszG8fGNEWpknC5wyZXZWgbaq7Zc7cAo,6131
7
+ citrascope/hardware/abstract_astro_hardware_adapter.py,sha256=Xc1zNuvlyYapWto37dzFfaKM62pKDN7VC8r4oGF8Up4,8140
8
8
  citrascope/hardware/adapter_registry.py,sha256=fFIZhXYphZ_p480c6hICpcx9fNOeX-EG2tvLHm372dM,3170
9
9
  citrascope/hardware/indi_adapter.py,sha256=uNrjkfxD0zjOPfar6J-frb6A87VkEjsL7SD9N9bEsC8,29903
10
10
  citrascope/hardware/kstars_dbus_adapter.py,sha256=Nv6ijVDvgTCTZUmRFh3Wh-YS7ChiztiXF17OWlzJwoo,7001
11
- citrascope/hardware/nina_adv_http_adapter.py,sha256=2McglZprEJQ8OKnqFXDrZCcl2AylkWO8Y1kGrS_nwtY,26261
11
+ citrascope/hardware/nina_adv_http_adapter.py,sha256=Jzg9j74bEFdY77XX-O-UE-e3Q3Y8PQ-xL7-igXMqbwg,27637
12
12
  citrascope/hardware/nina_adv_http_survey_template.json,sha256=beg4H6Bzby-0x5uDc_eRJQ_rKs8VT64sDJyAzS_q1l4,14424
13
13
  citrascope/logging/__init__.py,sha256=YU38HLMWfbXh_H-s7W7Zx2pbCR4f_tRk7z0G8xqz4_o,179
14
14
  citrascope/logging/_citrascope_logger.py,sha256=GkqNpFJWiatqrBr8t4o2nHt7V9bBDJ8mysM0F4AXMa8,3479
15
15
  citrascope/logging/web_log_handler.py,sha256=d0XQzHJZ5M1v3H351tdkBYg7EOwFzXpp7PA9nYejIV0,2659
16
16
  citrascope/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- citrascope/settings/citrascope_settings.py,sha256=yBDlM5uCnhUQgIrNDMTPX6cmgKnKl-lvra41-7XN40E,5966
17
+ citrascope/settings/citrascope_settings.py,sha256=C9BgX3vKDnsbwZF1NskUmkczB5ESdOSWPaHrq4rA0aQ,5635
18
18
  citrascope/settings/settings_file_manager.py,sha256=Yijb-I9hbbVJ2thkr7OrfkNknSPt1RDpsE7VvqAs0a8,4193
19
- citrascope/tasks/runner.py,sha256=xVnGWe_iYutBYqIhgOOriPjPcYUJ8IRbYRYKiAbehAQ,12584
19
+ citrascope/tasks/runner.py,sha256=-1xSrM2Kvckmn2dg3rs2w-ljsKAbCgrftdvEkZrGaKs,13697
20
20
  citrascope/tasks/task.py,sha256=0u0oN56E6KaNz19ba_7WuY43Sk4CTXc8UPT7sdUpRXo,1287
21
21
  citrascope/tasks/scope/base_telescope_task.py,sha256=wIdyUxplFNhf_YMdCXOK6pG7HF7tZn_id59TvYyWZAY,9674
22
22
  citrascope/tasks/scope/static_telescope_task.py,sha256=XP53zYVcyLHLvebDU06Jx0ghPK3tb0c_XmO60yj_XSA,1132
23
23
  citrascope/tasks/scope/tracking_telescope_task.py,sha256=k5LEmEi_xnFHNjqPNYb8_tqDdCFD3YGe25Wh_brJXHk,1130
24
24
  citrascope/web/__init__.py,sha256=CgU36fyNSxGXjUy3hsHwx7UxF8UO4Qsb7PjC9-6tRmY,38
25
- citrascope/web/app.py,sha256=OfBur9oM06NakrA30MOk9CgX0ewJ7IZl4kn0gtQgHJ4,20261
25
+ citrascope/web/app.py,sha256=LGimBCHWmaPrLkh6JDpJ7IoC__423lhO7NldQGTFzKI,26181
26
26
  citrascope/web/server.py,sha256=IJJk4HgEwcsjHercL-Q5z39NmJRbkNk_51HIUKKhtRE,5242
27
27
  citrascope/web/static/api.js,sha256=s-b1FIw-pTo3A8kLlLINVqHhIvfHwTWA7cEvz4N8Gqc,1924
28
- citrascope/web/static/app.js,sha256=xwKBzLB2uWytyUDdzrT6qsQBwIfESypjf8jSLnrQRZg,17820
29
- citrascope/web/static/config.js,sha256=4pjMH8yS24JkH62OpklEdPC0EAmd7e5c21WWETz1EO0,15701
28
+ citrascope/web/static/app.js,sha256=yvthXW68B0JedxuE8cF6HYfcH7ukDQ7g959o0DNhY80,26214
29
+ citrascope/web/static/config.js,sha256=l0brTgkQ4UW-C9a2wabkvVGwb-wPnJSmM4lb63s0KbY,21500
30
30
  citrascope/web/static/style.css,sha256=haVMnKlULZ-SL_qmLyqnrwHdX28s6KzVK9uE_R5nlLo,2877
31
31
  citrascope/web/static/websocket.js,sha256=UITw1DDfehOKpjlltn5MXhewZYGKzPFmaTtMFtC0-Ps,3931
32
32
  citrascope/web/static/img/citra.png,sha256=Bq8dPWB6fNz7a_H0FuEtNmZWcPHH2iV2OC-fMg4REbQ,205570
33
33
  citrascope/web/static/img/favicon.png,sha256=zrbUlpFXDB_zmsIdhhn8_klnc2Ma3N6Q8ouBMAxFjbM,24873
34
- citrascope/web/templates/dashboard.html,sha256=4ChTsnMMP6VyfdQT9bM4rRruYHJorAeZFOMDhI-j3fA,24155
35
- citrascope-0.4.0.dist-info/METADATA,sha256=eombd_1rHdc9ubLY6PM7M2UKBqLMeT8aOQDUtqqWMP0,6481
36
- citrascope-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
37
- citrascope-0.4.0.dist-info/entry_points.txt,sha256=fP22Lt8bNZ_whBowDnOWSADf_FUrgAWnIhqqPf5Xo2g,55
38
- citrascope-0.4.0.dist-info/RECORD,,
34
+ citrascope/web/templates/dashboard.html,sha256=7N5JPlihK3WNDe8fnFMfIRfCgp4ZZJLbl2TVc_nY0SU,30119
35
+ citrascope-0.5.0.dist-info/METADATA,sha256=dgATUjmZWMhRrItDYeCkZ-oy-AQz63BDdbeZHsWY12A,6481
36
+ citrascope-0.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
37
+ citrascope-0.5.0.dist-info/entry_points.txt,sha256=fP22Lt8bNZ_whBowDnOWSADf_FUrgAWnIhqqPf5Xo2g,55
38
+ citrascope-0.5.0.dist-info/RECORD,,