featrixsphere 0.2.1314__tar.gz → 0.2.1439__tar.gz
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.
- featrixsphere-0.2.1439/MANIFEST.in +8 -0
- {featrixsphere-0.2.1314/featrixsphere.egg-info → featrixsphere-0.2.1439}/PKG-INFO +1 -1
- featrixsphere-0.2.1439/VERSION +1 -0
- featrixsphere-0.2.1439/featrix-update.py +488 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/featrixsphere/__init__.py +1 -1
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/featrixsphere/client.py +113 -29
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439/featrixsphere.egg-info}/PKG-INFO +1 -1
- featrixsphere-0.2.1439/featrixsphere.egg-info/SOURCES.txt +168 -0
- featrixsphere-0.2.1439/nv-install.sh +169 -0
- featrixsphere-0.2.1439/src/api.py +9584 -0
- featrixsphere-0.2.1439/src/auto_upgrade_monitor.py +500 -0
- featrixsphere-0.2.1439/src/build_version.py +156 -0
- featrixsphere-0.2.1439/src/celery_app.py +553 -0
- featrixsphere-0.2.1439/src/cli.py +5853 -0
- featrixsphere-0.2.1439/src/config.py +70 -0
- featrixsphere-0.2.1439/src/demo_existing_model.py +334 -0
- featrixsphere-0.2.1439/src/demo_label_updates.py +259 -0
- featrixsphere-0.2.1439/src/deploy.py +153 -0
- featrixsphere-0.2.1439/src/deploy_cache_debug.sh +41 -0
- featrixsphere-0.2.1439/src/ensure_watchdog_running.sh +86 -0
- featrixsphere-0.2.1439/src/error_tracker.py +288 -0
- featrixsphere-0.2.1439/src/event_log.py +253 -0
- featrixsphere-0.2.1439/src/example_api_usage.py +195 -0
- featrixsphere-0.2.1439/src/example_prediction_feedback.py +149 -0
- featrixsphere-0.2.1439/src/example_train_predictor.py +105 -0
- featrixsphere-0.2.1439/src/featrix_queue.py +7112 -0
- featrixsphere-0.2.1439/src/featrix_watchdog.py +821 -0
- featrixsphere-0.2.1439/src/gc_cleanup.py +376 -0
- featrixsphere-0.2.1439/src/lib/convergence_monitor.py +598 -0
- featrixsphere-0.2.1439/src/lib/epoch_projections.py +367 -0
- featrixsphere-0.2.1439/src/lib/es_projections.py +116 -0
- featrixsphere-0.2.1439/src/lib/es_training.py +1847 -0
- featrixsphere-0.2.1439/src/lib/featrix/__init__.py +7 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/MetaDataCache.py +203 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/__init__.py +9 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/calibration_utils.py +802 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/classification_metrics.py +827 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/config.py +46 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/data_frame_data_set.py +206 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/dataloader_utils.py +377 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/detect.py +1515 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/device.py +40 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/domain_codec.py +471 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/dropout_scheduler.py +272 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/embedded_space.py +9768 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/embedding_lr_scheduler.py +372 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/embedding_space_utils.py +269 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/embedding_utils.py +38 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/encoders.py +1576 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/enrich.py +476 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/es_projection.py +454 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/exceptions.py +128 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/featrix_csv.py +520 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/featrix_json.py +86 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/featrix_module_dict.py +78 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/featrix_token.py +264 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/guardrails.py +269 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/hubspot_free_domains_list_may_2025.py +4779 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/input_data_file.py +160 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/input_data_set.py +1665 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/integrity.py +260 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/json_cache.py +276 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/json_codec.py +338 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/logging_config.py +52 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/mask_tracker.py +415 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/model_config.py +177 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/model_hash.py +71 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/movie_frame_task.py +192 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/network_viz.py +263 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/prng_control.py +27 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/demo_advisor_decisions.py +169 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/example_complete_workflow.py +229 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/generate_focal_report.py +881 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/model_advisor.py +530 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/show_results.py +217 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_adaptive_training.py +444 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_confusion_matrix_metadata.py +134 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_embedding_quality.py +351 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_embedding_space.py +329 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_extend_embedding_space.py +420 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_focal_comparison.py +389 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_focal_comparison_enhanced.py +395 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_focal_loss_single_predictor.py +131 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_label_smoothing.py +348 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_monitor_integration.py +189 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_piecewise_epochs.py +77 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_predict_during_training.py +324 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_timeline_quick.py +82 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_training_monitor.py +134 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/test_warning_tracking.py +90 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/qa/visualize_training_timeline.py +504 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/scalar_codec.py +1150 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/set_codec.py +825 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/setlist_codec.py +380 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/simple_mlp.py +221 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/single_predictor.py +6281 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/single_predictor_mlp.py +312 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/sphere_config.py +353 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/sqlite_utils.py +138 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/stopwatch.py +202 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/string_analysis.py +826 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/string_cache.py +968 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/string_codec.py +1775 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/string_list_codec.py +266 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/timestamp_codec.py +365 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/training_context_manager.py +90 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/training_event.py +0 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/training_exceptions.py +181 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/training_history_db.py +293 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/transformer_encoder.py +380 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/url_codec.py +357 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/url_parser.py +264 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/utils.py +650 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/vector_codec.py +200 -0
- featrixsphere-0.2.1439/src/lib/featrix/neural/world_data.py +333 -0
- featrixsphere-0.2.1439/src/lib/featrix_debug.py +233 -0
- featrixsphere-0.2.1439/src/lib/json_encoder_cache.py +143 -0
- featrixsphere-0.2.1439/src/lib/knn_training.py +110 -0
- featrixsphere-0.2.1439/src/lib/single_predictor_cv.py +257 -0
- featrixsphere-0.2.1439/src/lib/single_predictor_training.py +2661 -0
- featrixsphere-0.2.1439/src/lib/sphere_config.py +432 -0
- featrixsphere-0.2.1439/src/lib/structureddata.py +1590 -0
- featrixsphere-0.2.1439/src/lib/training_monitor.py +693 -0
- featrixsphere-0.2.1439/src/lib/utils.py +157 -0
- featrixsphere-0.2.1439/src/lib/vector_db.py +528 -0
- featrixsphere-0.2.1439/src/lib/webhook_helpers.py +242 -0
- featrixsphere-0.2.1439/src/lib/weightwatcher_tracking.py +860 -0
- featrixsphere-0.2.1439/src/llm_client.py +136 -0
- featrixsphere-0.2.1439/src/manage_churro.sh +124 -0
- featrixsphere-0.2.1439/src/neural.py +0 -0
- featrixsphere-0.2.1439/src/node-install.sh +2199 -0
- featrixsphere-0.2.1439/src/prediction_drift_monitor.py +346 -0
- featrixsphere-0.2.1439/src/prediction_persistence_worker.py +310 -0
- featrixsphere-0.2.1439/src/quick_test_deployment.sh +180 -0
- featrixsphere-0.2.1439/src/recreate_session.py +496 -0
- featrixsphere-0.2.1439/src/redis_prediction_cli.py +178 -0
- featrixsphere-0.2.1439/src/redis_prediction_store.py +170 -0
- featrixsphere-0.2.1439/src/regenerate_training_movie.py +268 -0
- featrixsphere-0.2.1439/src/render_sphere.py +110 -0
- featrixsphere-0.2.1439/src/restart_celery_worker.sh +52 -0
- featrixsphere-0.2.1439/src/run_api_server.sh +37 -0
- featrixsphere-0.2.1439/src/send_email.py +100 -0
- featrixsphere-0.2.1439/src/slack.py +176 -0
- featrixsphere-0.2.1439/src/standalone_prediction.py +452 -0
- featrixsphere-0.2.1439/src/start_celery_worker.sh +65 -0
- featrixsphere-0.2.1439/src/start_churro_server.sh +258 -0
- featrixsphere-0.2.1439/src/tail-watch.py +354 -0
- featrixsphere-0.2.1439/src/test_api_client.py +1185 -0
- featrixsphere-0.2.1439/src/test_complete_workflow.py +386 -0
- featrixsphere-0.2.1439/src/test_json_tables_prediction.py +314 -0
- featrixsphere-0.2.1439/src/test_redis_predictions.py +92 -0
- featrixsphere-0.2.1439/src/test_server_connection.py +132 -0
- featrixsphere-0.2.1439/src/test_session_models.py +170 -0
- featrixsphere-0.2.1439/src/test_single_predictor_api.py +267 -0
- featrixsphere-0.2.1439/src/test_upload_endpoint.py +131 -0
- featrixsphere-0.2.1439/src/tree.py +14 -0
- featrixsphere-0.2.1439/src/utils.py +14 -0
- featrixsphere-0.2.1439/src/version.py +344 -0
- featrixsphere-0.2.1439/system_monitor.py +1047 -0
- featrixsphere-0.2.1314/MANIFEST.in +0 -4
- featrixsphere-0.2.1314/VERSION +0 -1
- featrixsphere-0.2.1314/featrixsphere.egg-info/SOURCES.txt +0 -16
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/README.md +0 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/featrixsphere/cli.py +0 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/featrixsphere/test_client.py +0 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/featrixsphere.egg-info/dependency_links.txt +0 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/featrixsphere.egg-info/entry_points.txt +0 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/featrixsphere.egg-info/not-zip-safe +0 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/featrixsphere.egg-info/requires.txt +0 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/featrixsphere.egg-info/top_level.txt +0 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/requirements.txt +0 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/setup.cfg +0 -0
- {featrixsphere-0.2.1314 → featrixsphere-0.2.1439}/setup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.1439
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Featrix Sphere Update Script
|
|
4
|
+
|
|
5
|
+
Checks for newer firmware versions on the server and installs them.
|
|
6
|
+
Runs standalone on compute nodes - no dependencies on common libs.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 featrix-update.py [--dry-run]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import argparse
|
|
16
|
+
import subprocess
|
|
17
|
+
import hashlib
|
|
18
|
+
import tarfile
|
|
19
|
+
import tempfile
|
|
20
|
+
import shutil
|
|
21
|
+
import traceback
|
|
22
|
+
import socket
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from typing import Optional, Dict, Any
|
|
26
|
+
from urllib.request import urlopen, Request
|
|
27
|
+
from urllib.error import URLError
|
|
28
|
+
|
|
29
|
+
# Configuration
|
|
30
|
+
FIRMWARE_SERVER = "https://bits.featrix.com/sphere-firmware"
|
|
31
|
+
INDEX_URL = f"{FIRMWARE_SERVER}/index.json"
|
|
32
|
+
VERSION_FILE = Path("/sphere/VERSION")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_current_version() -> Optional[str]:
|
|
36
|
+
"""Get current installed version from VERSION file."""
|
|
37
|
+
if VERSION_FILE.exists():
|
|
38
|
+
try:
|
|
39
|
+
return VERSION_FILE.read_text().strip()
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"⚠️ Error reading VERSION file: {e}")
|
|
42
|
+
return None
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_current_version_hash() -> Optional[str]:
|
|
47
|
+
"""Get current version hash - not available on nodes (no git)."""
|
|
48
|
+
# Nodes don't have git, so we can't get version hash
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_node_name() -> str:
|
|
53
|
+
"""Get node name from hostname, similar to featrix_watchdog.py."""
|
|
54
|
+
try:
|
|
55
|
+
hostname = socket.gethostname()
|
|
56
|
+
hostname_lower = hostname.lower()
|
|
57
|
+
# Map hostname to node name (e.g., "taco", "churro", "burrito")
|
|
58
|
+
if 'taco' in hostname_lower:
|
|
59
|
+
return 'taco'
|
|
60
|
+
elif 'churro' in hostname_lower:
|
|
61
|
+
return 'churro'
|
|
62
|
+
elif 'burrito' in hostname_lower:
|
|
63
|
+
return 'burrito'
|
|
64
|
+
else:
|
|
65
|
+
return hostname.split('.')[0] # Use first part of hostname
|
|
66
|
+
except Exception:
|
|
67
|
+
return 'unknown'
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def announce_to_sphere_api(version: str, version_hash: Optional[str] = None, status: str = "available") -> bool:
|
|
71
|
+
"""Announce node existence to sphere-api /compute-nodes/announce endpoint."""
|
|
72
|
+
try:
|
|
73
|
+
import urllib.request
|
|
74
|
+
import urllib.parse
|
|
75
|
+
|
|
76
|
+
node_name = get_node_name()
|
|
77
|
+
|
|
78
|
+
# Get version hash if not provided
|
|
79
|
+
if not version_hash:
|
|
80
|
+
# Try to read from common locations
|
|
81
|
+
for hash_path in [
|
|
82
|
+
Path("/tmp/SPHERE_GIT_HASH"),
|
|
83
|
+
Path("/sphere/VERSION_HASH"),
|
|
84
|
+
Path("/sphere/app/VERSION_HASH"),
|
|
85
|
+
]:
|
|
86
|
+
if hash_path.exists():
|
|
87
|
+
try:
|
|
88
|
+
version_hash = hash_path.read_text().strip()[:8]
|
|
89
|
+
break
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
payload = {
|
|
94
|
+
"node_name": node_name,
|
|
95
|
+
"status": status,
|
|
96
|
+
"node_timestamp_now": datetime.now().isoformat(),
|
|
97
|
+
"version": version,
|
|
98
|
+
"version_hash": version_hash or "unknown"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Try to detect external IP and port
|
|
102
|
+
try:
|
|
103
|
+
# Try to get external IP
|
|
104
|
+
external_ip = None
|
|
105
|
+
try:
|
|
106
|
+
response = urlopen("https://api.ipify.org", timeout=5)
|
|
107
|
+
external_ip = response.read().decode('utf-8').strip()
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
if external_ip:
|
|
112
|
+
payload["external_ip"] = external_ip
|
|
113
|
+
|
|
114
|
+
# Default port is 8000
|
|
115
|
+
payload["port"] = 8000
|
|
116
|
+
except Exception:
|
|
117
|
+
pass # IP/port detection is optional
|
|
118
|
+
|
|
119
|
+
# Post to sphere-api
|
|
120
|
+
api_url = "https://sphere-api.featrix.com/compute-nodes/announce"
|
|
121
|
+
data = json.dumps(payload).encode('utf-8')
|
|
122
|
+
|
|
123
|
+
# Create custom User-Agent with version and hostname
|
|
124
|
+
user_agent = f"Featrix Firmware v{version} ({node_name})"
|
|
125
|
+
req = Request(api_url, data=data, headers={
|
|
126
|
+
'Content-Type': 'application/json',
|
|
127
|
+
'User-Agent': user_agent
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
response = urlopen(req, timeout=10)
|
|
132
|
+
if response.getcode() == 200:
|
|
133
|
+
print(f"✅ Announced node '{node_name}' to sphere-api (status: {status}, version: {version})")
|
|
134
|
+
return True
|
|
135
|
+
else:
|
|
136
|
+
print(f"⚠️ sphere-api announcement returned {response.getcode()}")
|
|
137
|
+
return False
|
|
138
|
+
except URLError as e:
|
|
139
|
+
print(f"⚠️ Failed to announce to sphere-api: {e}")
|
|
140
|
+
return False
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print(f"⚠️ Error announcing to sphere-api: {e}")
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def fetch_index() -> Optional[Dict[str, Any]]:
|
|
147
|
+
"""Fetch index.json from firmware server."""
|
|
148
|
+
try:
|
|
149
|
+
print(f"📡 Fetching firmware index from {INDEX_URL}...")
|
|
150
|
+
req = Request(INDEX_URL)
|
|
151
|
+
req.add_header('User-Agent', 'Featrix-Update-Script/1.0')
|
|
152
|
+
|
|
153
|
+
with urlopen(req, timeout=10) as response:
|
|
154
|
+
data = json.loads(response.read().decode('utf-8'))
|
|
155
|
+
print(f"✅ Fetched index with {data.get('total_files', 0)} files")
|
|
156
|
+
return data
|
|
157
|
+
except URLError as e:
|
|
158
|
+
print(f"❌ Failed to fetch index: {e}")
|
|
159
|
+
return None
|
|
160
|
+
except json.JSONDecodeError as e:
|
|
161
|
+
print(f"❌ Failed to parse index.json: {e}")
|
|
162
|
+
return None
|
|
163
|
+
except Exception as e:
|
|
164
|
+
print(f"❌ Error fetching index: {e}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def compare_versions(current: str, available: str) -> int:
|
|
169
|
+
"""
|
|
170
|
+
Compare version strings (semantic versioning).
|
|
171
|
+
Returns: -1 if current < available, 0 if equal, 1 if current > available
|
|
172
|
+
"""
|
|
173
|
+
def version_tuple(v: str) -> tuple:
|
|
174
|
+
"""Convert version string to tuple for comparison."""
|
|
175
|
+
parts = v.split('.')
|
|
176
|
+
try:
|
|
177
|
+
return tuple(int(p) for p in parts[:3]) # Major.minor.patch
|
|
178
|
+
except ValueError:
|
|
179
|
+
return (0, 0, 0)
|
|
180
|
+
|
|
181
|
+
current_tuple = version_tuple(current)
|
|
182
|
+
available_tuple = version_tuple(available)
|
|
183
|
+
|
|
184
|
+
if current_tuple < available_tuple:
|
|
185
|
+
return -1
|
|
186
|
+
elif current_tuple > available_tuple:
|
|
187
|
+
return 1
|
|
188
|
+
else:
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def find_newest_version(index: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
193
|
+
"""Find the newest version from index.json."""
|
|
194
|
+
files = index.get('files', [])
|
|
195
|
+
if not files:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
# Filter to only files (not directories) with valid versions
|
|
199
|
+
versioned_files = []
|
|
200
|
+
for file_info in files:
|
|
201
|
+
if file_info.get('is_directory'):
|
|
202
|
+
continue
|
|
203
|
+
version = file_info.get('version')
|
|
204
|
+
if version and version != 'unknown':
|
|
205
|
+
versioned_files.append(file_info)
|
|
206
|
+
|
|
207
|
+
if not versioned_files:
|
|
208
|
+
print("⚠️ No files with version information found")
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
# Sort by version (newest first)
|
|
212
|
+
versioned_files.sort(
|
|
213
|
+
key=lambda x: tuple(int(p) for p in x['version'].split('.')[:3]),
|
|
214
|
+
reverse=True
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return versioned_files[0]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def install_package(package_file: Path, force: bool = False) -> bool:
|
|
221
|
+
"""
|
|
222
|
+
Install a sphere-app package tarball.
|
|
223
|
+
Self-contained - extracts package and runs node-install.sh from extracted directory.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
print(f"\n🚀 Installing {package_file.name}...")
|
|
227
|
+
|
|
228
|
+
# Create temp directory for extraction
|
|
229
|
+
temp_dir = Path(tempfile.mkdtemp())
|
|
230
|
+
try:
|
|
231
|
+
print(f" Extracting to temporary directory: {temp_dir}")
|
|
232
|
+
|
|
233
|
+
# Extract tarball
|
|
234
|
+
with tarfile.open(package_file, 'r:gz') as tar:
|
|
235
|
+
tar.extractall(temp_dir)
|
|
236
|
+
|
|
237
|
+
# Find the sphere-app directory in the extracted content
|
|
238
|
+
extracted_dir = None
|
|
239
|
+
if (temp_dir / "sphere-app").exists():
|
|
240
|
+
extracted_dir = temp_dir / "sphere-app"
|
|
241
|
+
elif temp_dir.exists():
|
|
242
|
+
# Check if node-install.sh is directly in temp_dir
|
|
243
|
+
if (temp_dir / "node-install.sh").exists() or (temp_dir / "src" / "node-install.sh").exists():
|
|
244
|
+
extracted_dir = temp_dir
|
|
245
|
+
else:
|
|
246
|
+
# Look for any subdirectory with node-install.sh
|
|
247
|
+
for item in temp_dir.iterdir():
|
|
248
|
+
if item.is_dir():
|
|
249
|
+
if (item / "node-install.sh").exists() or (item / "src" / "node-install.sh").exists():
|
|
250
|
+
extracted_dir = item
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
if not extracted_dir:
|
|
254
|
+
print("❌ Could not find sphere-app directory or node-install.sh in extracted package")
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
print(f" Package extracted successfully")
|
|
258
|
+
|
|
259
|
+
# Show version info if available
|
|
260
|
+
package_version = "unknown"
|
|
261
|
+
package_hash = "unknown"
|
|
262
|
+
if (extracted_dir / "VERSION").exists():
|
|
263
|
+
package_version = (extracted_dir / "VERSION").read_text().strip()
|
|
264
|
+
if (extracted_dir / "VERSION_HASH").exists():
|
|
265
|
+
package_hash = (extracted_dir / "VERSION_HASH").read_text().strip()
|
|
266
|
+
|
|
267
|
+
print(f" Package version: {package_version}")
|
|
268
|
+
print(f" Package hash: {package_hash}")
|
|
269
|
+
|
|
270
|
+
# Check if already deployed (unless forced)
|
|
271
|
+
if not force:
|
|
272
|
+
deployed_hash = None
|
|
273
|
+
if Path("/sphere/app/VERSION_HASH").exists():
|
|
274
|
+
deployed_hash = Path("/sphere/app/VERSION_HASH").read_text().strip()
|
|
275
|
+
|
|
276
|
+
if deployed_hash and package_hash != "unknown" and package_hash == deployed_hash:
|
|
277
|
+
print(f"\n⏭️ This package is already deployed (hash: {package_hash})")
|
|
278
|
+
print(f" Skipping installation. Use --force to reinstall anyway.")
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
# Find node-install.sh
|
|
282
|
+
install_script = None
|
|
283
|
+
for script_path in [
|
|
284
|
+
extracted_dir / "node-install.sh",
|
|
285
|
+
extracted_dir / "src" / "node-install.sh",
|
|
286
|
+
]:
|
|
287
|
+
if script_path.exists():
|
|
288
|
+
install_script = script_path
|
|
289
|
+
break
|
|
290
|
+
|
|
291
|
+
if not install_script:
|
|
292
|
+
print("❌ node-install.sh not found in package")
|
|
293
|
+
print(" Package contents:")
|
|
294
|
+
for item in extracted_dir.iterdir():
|
|
295
|
+
print(f" {item.name}")
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
print(f" Using install script: {install_script}")
|
|
299
|
+
|
|
300
|
+
# Change to extracted directory and run install script
|
|
301
|
+
original_cwd = os.getcwd()
|
|
302
|
+
try:
|
|
303
|
+
os.chdir(extracted_dir)
|
|
304
|
+
|
|
305
|
+
# Always use --force when installing from a package because:
|
|
306
|
+
# 1. We've already done version checking in featrix-update.py
|
|
307
|
+
# 2. The package is a specific version we want to install
|
|
308
|
+
# 3. node-install.sh checks git state from /home/mitch/sphere which may not match the package
|
|
309
|
+
cmd = ["sudo", str(install_script), "--force"]
|
|
310
|
+
print(f" Using --force flag (installing from package)")
|
|
311
|
+
|
|
312
|
+
# Run the install script with sudo
|
|
313
|
+
result = subprocess.run(
|
|
314
|
+
cmd,
|
|
315
|
+
check=True,
|
|
316
|
+
timeout=600 # 10 minute timeout
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
print("✅ Installation completed successfully")
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
finally:
|
|
323
|
+
os.chdir(original_cwd)
|
|
324
|
+
|
|
325
|
+
except subprocess.CalledProcessError as e:
|
|
326
|
+
print(f"❌ Installation failed with exit code {e.returncode}")
|
|
327
|
+
return False
|
|
328
|
+
except subprocess.TimeoutExpired:
|
|
329
|
+
print("❌ Installation timed out after 10 minutes")
|
|
330
|
+
return False
|
|
331
|
+
except Exception as e:
|
|
332
|
+
print(f"❌ Installation error: {e}")
|
|
333
|
+
traceback.print_exc()
|
|
334
|
+
return False
|
|
335
|
+
finally:
|
|
336
|
+
# Clean up temp directory
|
|
337
|
+
if temp_dir.exists():
|
|
338
|
+
print(f" Cleaning up temporary files...")
|
|
339
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def download_file(url: str, dest_path: Path, expected_hash: Optional[str] = None) -> bool:
|
|
343
|
+
"""Download a file and verify its hash if provided."""
|
|
344
|
+
try:
|
|
345
|
+
print(f"📥 Downloading {url}...")
|
|
346
|
+
req = Request(url)
|
|
347
|
+
req.add_header('User-Agent', 'Featrix-Update-Script/1.0')
|
|
348
|
+
|
|
349
|
+
with urlopen(req, timeout=300) as response:
|
|
350
|
+
data = response.read()
|
|
351
|
+
|
|
352
|
+
# Verify hash if provided
|
|
353
|
+
if expected_hash:
|
|
354
|
+
calculated_hash = hashlib.sha256(data).hexdigest()
|
|
355
|
+
if calculated_hash != expected_hash:
|
|
356
|
+
print(f"❌ Hash mismatch! Expected {expected_hash[:16]}..., got {calculated_hash[:16]}...")
|
|
357
|
+
return False
|
|
358
|
+
print(f"✅ Hash verified: {expected_hash[:16]}...")
|
|
359
|
+
|
|
360
|
+
# Write to destination
|
|
361
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
362
|
+
with open(dest_path, 'wb') as f:
|
|
363
|
+
f.write(data)
|
|
364
|
+
|
|
365
|
+
file_size_mb = len(data) / (1024 * 1024)
|
|
366
|
+
print(f"✅ Downloaded {file_size_mb:.2f} MB to {dest_path}")
|
|
367
|
+
return True
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
print(f"❌ Download failed: {e}")
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def main():
|
|
375
|
+
"""Main entry point."""
|
|
376
|
+
# Save original working directory to restore it at the end
|
|
377
|
+
original_cwd = os.getcwd()
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
parser = argparse.ArgumentParser(description='Check for and install Featrix Sphere updates')
|
|
381
|
+
parser.add_argument('--dry-run', action='store_true', help='Show what would be done without installing')
|
|
382
|
+
parser.add_argument('--force', action='store_true', help='Force update even if already on latest version')
|
|
383
|
+
args = parser.parse_args()
|
|
384
|
+
|
|
385
|
+
print("=" * 60)
|
|
386
|
+
print("🔄 Featrix Sphere Update Checker")
|
|
387
|
+
print("=" * 60)
|
|
388
|
+
|
|
389
|
+
# Get current version
|
|
390
|
+
current_version = get_current_version()
|
|
391
|
+
|
|
392
|
+
if current_version:
|
|
393
|
+
print(f"📦 Current version: {current_version}")
|
|
394
|
+
else:
|
|
395
|
+
print("⚠️ Could not determine current version (continuing anyway)")
|
|
396
|
+
|
|
397
|
+
# Fetch index
|
|
398
|
+
index = fetch_index()
|
|
399
|
+
if not index:
|
|
400
|
+
print("❌ Failed to fetch firmware index")
|
|
401
|
+
return 1
|
|
402
|
+
|
|
403
|
+
# Find newest version
|
|
404
|
+
newest = find_newest_version(index)
|
|
405
|
+
if not newest:
|
|
406
|
+
print("❌ No valid firmware files found")
|
|
407
|
+
return 1
|
|
408
|
+
|
|
409
|
+
newest_version = newest['version']
|
|
410
|
+
newest_filename = newest['filename']
|
|
411
|
+
newest_hash = newest.get('hash')
|
|
412
|
+
newest_url = f"{FIRMWARE_SERVER}/{newest_filename}"
|
|
413
|
+
|
|
414
|
+
print(f"\n📊 Available versions:")
|
|
415
|
+
print(f" Newest: {newest_version} ({newest_filename})")
|
|
416
|
+
if newest_hash:
|
|
417
|
+
print(f" Hash: {newest_hash[:16]}...")
|
|
418
|
+
|
|
419
|
+
# Compare versions
|
|
420
|
+
if not current_version:
|
|
421
|
+
print("\n⚠️ Cannot compare versions - current version unknown")
|
|
422
|
+
should_update = True
|
|
423
|
+
else:
|
|
424
|
+
comparison = compare_versions(current_version, newest_version)
|
|
425
|
+
if comparison < 0:
|
|
426
|
+
print(f"\n✅ Update available: {current_version} → {newest_version}")
|
|
427
|
+
should_update = True
|
|
428
|
+
elif comparison == 0:
|
|
429
|
+
print(f"\n✅ Already on latest version: {current_version}")
|
|
430
|
+
if args.force:
|
|
431
|
+
print(" --force flag set, will reinstall anyway")
|
|
432
|
+
should_update = True
|
|
433
|
+
else:
|
|
434
|
+
should_update = False
|
|
435
|
+
else:
|
|
436
|
+
print(f"\n⚠️ Current version ({current_version}) is newer than available ({newest_version})")
|
|
437
|
+
if args.force:
|
|
438
|
+
print(" --force flag set, will downgrade anyway")
|
|
439
|
+
should_update = True
|
|
440
|
+
else:
|
|
441
|
+
should_update = False
|
|
442
|
+
|
|
443
|
+
if not should_update:
|
|
444
|
+
print("No update needed.")
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
if args.dry_run:
|
|
448
|
+
print("\n🔍 DRY RUN - Would perform the following:")
|
|
449
|
+
print(f" 1. Download {newest_filename} from {newest_url}")
|
|
450
|
+
if newest_hash:
|
|
451
|
+
print(f" 2. Verify hash: {newest_hash[:16]}...")
|
|
452
|
+
print(f" 3. Extract package and run node-install.sh")
|
|
453
|
+
print("\nRun without --dry-run to perform update.")
|
|
454
|
+
return 0
|
|
455
|
+
|
|
456
|
+
# Download file
|
|
457
|
+
download_dir = Path.home() / "featrix-updates"
|
|
458
|
+
download_dir.mkdir(exist_ok=True)
|
|
459
|
+
downloaded_file = download_dir / newest_filename
|
|
460
|
+
|
|
461
|
+
if not download_file(newest_url, downloaded_file, newest_hash):
|
|
462
|
+
print("❌ Download failed")
|
|
463
|
+
return 1
|
|
464
|
+
|
|
465
|
+
# Install package (self-contained - no external script needed)
|
|
466
|
+
if not install_package(downloaded_file, force=args.force):
|
|
467
|
+
print("❌ Installation failed")
|
|
468
|
+
return 1
|
|
469
|
+
|
|
470
|
+
# Announce node to sphere-api after successful installation
|
|
471
|
+
print("\n📡 Announcing node to sphere-api...")
|
|
472
|
+
installed_version = get_current_version() or newest_version
|
|
473
|
+
installed_hash = newest_hash[:8] if newest_hash else None
|
|
474
|
+
announce_to_sphere_api(installed_version, installed_hash, status="available")
|
|
475
|
+
|
|
476
|
+
return 0
|
|
477
|
+
|
|
478
|
+
finally:
|
|
479
|
+
# Always restore original working directory
|
|
480
|
+
try:
|
|
481
|
+
os.chdir(original_cwd)
|
|
482
|
+
except Exception:
|
|
483
|
+
pass # Ignore errors when restoring directory
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
if __name__ == "__main__":
|
|
487
|
+
exit(main())
|
|
488
|
+
|