featrixsphere 0.2.1238__tar.gz → 0.2.1438__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.
Files changed (173) hide show
  1. featrixsphere-0.2.1438/MANIFEST.in +8 -0
  2. {featrixsphere-0.2.1238/featrixsphere.egg-info → featrixsphere-0.2.1438}/PKG-INFO +1 -1
  3. featrixsphere-0.2.1438/VERSION +1 -0
  4. featrixsphere-0.2.1438/featrix-update.py +488 -0
  5. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/featrixsphere/__init__.py +1 -1
  6. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/featrixsphere/client.py +116 -70
  7. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438/featrixsphere.egg-info}/PKG-INFO +1 -1
  8. featrixsphere-0.2.1438/featrixsphere.egg-info/SOURCES.txt +168 -0
  9. featrixsphere-0.2.1438/nv-install.sh +169 -0
  10. featrixsphere-0.2.1438/src/api.py +9584 -0
  11. featrixsphere-0.2.1438/src/auto_upgrade_monitor.py +500 -0
  12. featrixsphere-0.2.1438/src/build_version.py +156 -0
  13. featrixsphere-0.2.1438/src/celery_app.py +553 -0
  14. featrixsphere-0.2.1438/src/cli.py +5853 -0
  15. featrixsphere-0.2.1438/src/config.py +70 -0
  16. featrixsphere-0.2.1438/src/demo_existing_model.py +334 -0
  17. featrixsphere-0.2.1438/src/demo_label_updates.py +259 -0
  18. featrixsphere-0.2.1438/src/deploy.py +153 -0
  19. featrixsphere-0.2.1438/src/deploy_cache_debug.sh +41 -0
  20. featrixsphere-0.2.1438/src/ensure_watchdog_running.sh +86 -0
  21. featrixsphere-0.2.1438/src/error_tracker.py +288 -0
  22. featrixsphere-0.2.1438/src/event_log.py +253 -0
  23. featrixsphere-0.2.1438/src/example_api_usage.py +195 -0
  24. featrixsphere-0.2.1438/src/example_prediction_feedback.py +149 -0
  25. featrixsphere-0.2.1438/src/example_train_predictor.py +105 -0
  26. featrixsphere-0.2.1438/src/featrix_queue.py +7112 -0
  27. featrixsphere-0.2.1438/src/featrix_watchdog.py +821 -0
  28. featrixsphere-0.2.1438/src/gc_cleanup.py +376 -0
  29. featrixsphere-0.2.1438/src/lib/convergence_monitor.py +598 -0
  30. featrixsphere-0.2.1438/src/lib/epoch_projections.py +367 -0
  31. featrixsphere-0.2.1438/src/lib/es_projections.py +116 -0
  32. featrixsphere-0.2.1438/src/lib/es_training.py +1847 -0
  33. featrixsphere-0.2.1438/src/lib/featrix/__init__.py +7 -0
  34. featrixsphere-0.2.1438/src/lib/featrix/neural/MetaDataCache.py +203 -0
  35. featrixsphere-0.2.1438/src/lib/featrix/neural/__init__.py +9 -0
  36. featrixsphere-0.2.1438/src/lib/featrix/neural/calibration_utils.py +802 -0
  37. featrixsphere-0.2.1438/src/lib/featrix/neural/classification_metrics.py +827 -0
  38. featrixsphere-0.2.1438/src/lib/featrix/neural/config.py +46 -0
  39. featrixsphere-0.2.1438/src/lib/featrix/neural/data_frame_data_set.py +206 -0
  40. featrixsphere-0.2.1438/src/lib/featrix/neural/dataloader_utils.py +377 -0
  41. featrixsphere-0.2.1438/src/lib/featrix/neural/detect.py +1515 -0
  42. featrixsphere-0.2.1438/src/lib/featrix/neural/device.py +40 -0
  43. featrixsphere-0.2.1438/src/lib/featrix/neural/domain_codec.py +471 -0
  44. featrixsphere-0.2.1438/src/lib/featrix/neural/dropout_scheduler.py +272 -0
  45. featrixsphere-0.2.1438/src/lib/featrix/neural/embedded_space.py +9768 -0
  46. featrixsphere-0.2.1438/src/lib/featrix/neural/embedding_lr_scheduler.py +372 -0
  47. featrixsphere-0.2.1438/src/lib/featrix/neural/embedding_space_utils.py +269 -0
  48. featrixsphere-0.2.1438/src/lib/featrix/neural/embedding_utils.py +38 -0
  49. featrixsphere-0.2.1438/src/lib/featrix/neural/encoders.py +1576 -0
  50. featrixsphere-0.2.1438/src/lib/featrix/neural/enrich.py +476 -0
  51. featrixsphere-0.2.1438/src/lib/featrix/neural/es_projection.py +454 -0
  52. featrixsphere-0.2.1438/src/lib/featrix/neural/exceptions.py +128 -0
  53. featrixsphere-0.2.1438/src/lib/featrix/neural/featrix_csv.py +520 -0
  54. featrixsphere-0.2.1438/src/lib/featrix/neural/featrix_json.py +86 -0
  55. featrixsphere-0.2.1438/src/lib/featrix/neural/featrix_module_dict.py +78 -0
  56. featrixsphere-0.2.1438/src/lib/featrix/neural/featrix_token.py +264 -0
  57. featrixsphere-0.2.1438/src/lib/featrix/neural/guardrails.py +269 -0
  58. featrixsphere-0.2.1438/src/lib/featrix/neural/hubspot_free_domains_list_may_2025.py +4779 -0
  59. featrixsphere-0.2.1438/src/lib/featrix/neural/input_data_file.py +160 -0
  60. featrixsphere-0.2.1438/src/lib/featrix/neural/input_data_set.py +1665 -0
  61. featrixsphere-0.2.1438/src/lib/featrix/neural/integrity.py +260 -0
  62. featrixsphere-0.2.1438/src/lib/featrix/neural/json_cache.py +276 -0
  63. featrixsphere-0.2.1438/src/lib/featrix/neural/json_codec.py +338 -0
  64. featrixsphere-0.2.1438/src/lib/featrix/neural/logging_config.py +52 -0
  65. featrixsphere-0.2.1438/src/lib/featrix/neural/mask_tracker.py +415 -0
  66. featrixsphere-0.2.1438/src/lib/featrix/neural/model_config.py +177 -0
  67. featrixsphere-0.2.1438/src/lib/featrix/neural/model_hash.py +71 -0
  68. featrixsphere-0.2.1438/src/lib/featrix/neural/movie_frame_task.py +192 -0
  69. featrixsphere-0.2.1438/src/lib/featrix/neural/network_viz.py +263 -0
  70. featrixsphere-0.2.1438/src/lib/featrix/neural/prng_control.py +27 -0
  71. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/demo_advisor_decisions.py +169 -0
  72. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/example_complete_workflow.py +229 -0
  73. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/generate_focal_report.py +881 -0
  74. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/model_advisor.py +530 -0
  75. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/show_results.py +217 -0
  76. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_adaptive_training.py +444 -0
  77. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_confusion_matrix_metadata.py +134 -0
  78. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_embedding_quality.py +351 -0
  79. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_embedding_space.py +329 -0
  80. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_extend_embedding_space.py +420 -0
  81. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_focal_comparison.py +389 -0
  82. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_focal_comparison_enhanced.py +395 -0
  83. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_focal_loss_single_predictor.py +131 -0
  84. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_label_smoothing.py +348 -0
  85. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_monitor_integration.py +189 -0
  86. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_piecewise_epochs.py +77 -0
  87. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_predict_during_training.py +324 -0
  88. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_timeline_quick.py +82 -0
  89. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_training_monitor.py +134 -0
  90. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/test_warning_tracking.py +90 -0
  91. featrixsphere-0.2.1438/src/lib/featrix/neural/qa/visualize_training_timeline.py +504 -0
  92. featrixsphere-0.2.1438/src/lib/featrix/neural/scalar_codec.py +1150 -0
  93. featrixsphere-0.2.1438/src/lib/featrix/neural/set_codec.py +825 -0
  94. featrixsphere-0.2.1438/src/lib/featrix/neural/setlist_codec.py +380 -0
  95. featrixsphere-0.2.1438/src/lib/featrix/neural/simple_mlp.py +221 -0
  96. featrixsphere-0.2.1438/src/lib/featrix/neural/single_predictor.py +6281 -0
  97. featrixsphere-0.2.1438/src/lib/featrix/neural/single_predictor_mlp.py +312 -0
  98. featrixsphere-0.2.1438/src/lib/featrix/neural/sphere_config.py +353 -0
  99. featrixsphere-0.2.1438/src/lib/featrix/neural/sqlite_utils.py +138 -0
  100. featrixsphere-0.2.1438/src/lib/featrix/neural/stopwatch.py +202 -0
  101. featrixsphere-0.2.1438/src/lib/featrix/neural/string_analysis.py +826 -0
  102. featrixsphere-0.2.1438/src/lib/featrix/neural/string_cache.py +968 -0
  103. featrixsphere-0.2.1438/src/lib/featrix/neural/string_codec.py +1775 -0
  104. featrixsphere-0.2.1438/src/lib/featrix/neural/string_list_codec.py +266 -0
  105. featrixsphere-0.2.1438/src/lib/featrix/neural/timestamp_codec.py +365 -0
  106. featrixsphere-0.2.1438/src/lib/featrix/neural/training_context_manager.py +90 -0
  107. featrixsphere-0.2.1438/src/lib/featrix/neural/training_event.py +0 -0
  108. featrixsphere-0.2.1438/src/lib/featrix/neural/training_exceptions.py +181 -0
  109. featrixsphere-0.2.1438/src/lib/featrix/neural/training_history_db.py +293 -0
  110. featrixsphere-0.2.1438/src/lib/featrix/neural/transformer_encoder.py +380 -0
  111. featrixsphere-0.2.1438/src/lib/featrix/neural/url_codec.py +357 -0
  112. featrixsphere-0.2.1438/src/lib/featrix/neural/url_parser.py +264 -0
  113. featrixsphere-0.2.1438/src/lib/featrix/neural/utils.py +650 -0
  114. featrixsphere-0.2.1438/src/lib/featrix/neural/vector_codec.py +200 -0
  115. featrixsphere-0.2.1438/src/lib/featrix/neural/world_data.py +333 -0
  116. featrixsphere-0.2.1438/src/lib/featrix_debug.py +233 -0
  117. featrixsphere-0.2.1438/src/lib/json_encoder_cache.py +143 -0
  118. featrixsphere-0.2.1438/src/lib/knn_training.py +110 -0
  119. featrixsphere-0.2.1438/src/lib/single_predictor_cv.py +257 -0
  120. featrixsphere-0.2.1438/src/lib/single_predictor_training.py +2661 -0
  121. featrixsphere-0.2.1438/src/lib/sphere_config.py +432 -0
  122. featrixsphere-0.2.1438/src/lib/structureddata.py +1590 -0
  123. featrixsphere-0.2.1438/src/lib/training_monitor.py +693 -0
  124. featrixsphere-0.2.1438/src/lib/utils.py +157 -0
  125. featrixsphere-0.2.1438/src/lib/vector_db.py +528 -0
  126. featrixsphere-0.2.1438/src/lib/webhook_helpers.py +242 -0
  127. featrixsphere-0.2.1438/src/lib/weightwatcher_tracking.py +860 -0
  128. featrixsphere-0.2.1438/src/llm_client.py +136 -0
  129. featrixsphere-0.2.1438/src/manage_churro.sh +124 -0
  130. featrixsphere-0.2.1438/src/neural.py +0 -0
  131. featrixsphere-0.2.1438/src/node-install.sh +2199 -0
  132. featrixsphere-0.2.1438/src/prediction_drift_monitor.py +346 -0
  133. featrixsphere-0.2.1438/src/prediction_persistence_worker.py +310 -0
  134. featrixsphere-0.2.1438/src/quick_test_deployment.sh +180 -0
  135. featrixsphere-0.2.1438/src/recreate_session.py +496 -0
  136. featrixsphere-0.2.1438/src/redis_prediction_cli.py +178 -0
  137. featrixsphere-0.2.1438/src/redis_prediction_store.py +170 -0
  138. featrixsphere-0.2.1438/src/regenerate_training_movie.py +268 -0
  139. featrixsphere-0.2.1438/src/render_sphere.py +110 -0
  140. featrixsphere-0.2.1438/src/restart_celery_worker.sh +52 -0
  141. featrixsphere-0.2.1438/src/run_api_server.sh +37 -0
  142. featrixsphere-0.2.1438/src/send_email.py +100 -0
  143. featrixsphere-0.2.1438/src/slack.py +176 -0
  144. featrixsphere-0.2.1438/src/standalone_prediction.py +452 -0
  145. featrixsphere-0.2.1438/src/start_celery_worker.sh +65 -0
  146. featrixsphere-0.2.1438/src/start_churro_server.sh +258 -0
  147. featrixsphere-0.2.1438/src/tail-watch.py +354 -0
  148. featrixsphere-0.2.1438/src/test_api_client.py +1185 -0
  149. featrixsphere-0.2.1438/src/test_complete_workflow.py +386 -0
  150. featrixsphere-0.2.1438/src/test_json_tables_prediction.py +314 -0
  151. featrixsphere-0.2.1438/src/test_redis_predictions.py +92 -0
  152. featrixsphere-0.2.1438/src/test_server_connection.py +132 -0
  153. featrixsphere-0.2.1438/src/test_session_models.py +170 -0
  154. featrixsphere-0.2.1438/src/test_single_predictor_api.py +267 -0
  155. featrixsphere-0.2.1438/src/test_upload_endpoint.py +131 -0
  156. featrixsphere-0.2.1438/src/tree.py +14 -0
  157. featrixsphere-0.2.1438/src/utils.py +14 -0
  158. featrixsphere-0.2.1438/src/version.py +344 -0
  159. featrixsphere-0.2.1438/system_monitor.py +1047 -0
  160. featrixsphere-0.2.1238/MANIFEST.in +0 -4
  161. featrixsphere-0.2.1238/VERSION +0 -1
  162. featrixsphere-0.2.1238/featrixsphere.egg-info/SOURCES.txt +0 -16
  163. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/README.md +0 -0
  164. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/featrixsphere/cli.py +0 -0
  165. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/featrixsphere/test_client.py +0 -0
  166. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/featrixsphere.egg-info/dependency_links.txt +0 -0
  167. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/featrixsphere.egg-info/entry_points.txt +0 -0
  168. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/featrixsphere.egg-info/not-zip-safe +0 -0
  169. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/featrixsphere.egg-info/requires.txt +0 -0
  170. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/featrixsphere.egg-info/top_level.txt +0 -0
  171. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/requirements.txt +0 -0
  172. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/setup.cfg +0 -0
  173. {featrixsphere-0.2.1238 → featrixsphere-0.2.1438}/setup.py +0 -0
@@ -0,0 +1,8 @@
1
+ include VERSION
2
+ include README.md
3
+ include requirements.txt
4
+ include system_monitor.py
5
+ include nv-install.sh
6
+ include featrix-update.py
7
+ recursive-include featrixsphere *.py
8
+ recursive-include src *.py *.sh
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: featrixsphere
3
- Version: 0.2.1238
3
+ Version: 0.2.1438
4
4
  Summary: Transform any CSV into a production-ready ML model in minutes, not months.
5
5
  Home-page: https://github.com/Featrix/sphere
6
6
  Author: Featrix
@@ -0,0 +1 @@
1
+ 0.2.1438
@@ -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
+
@@ -38,7 +38,7 @@ Example:
38
38
  ... labels=['Experiment A', 'Experiment B'])
39
39
  """
40
40
 
41
- __version__ = "0.2.1238"
41
+ __version__ = "0.2.1438"
42
42
  __author__ = "Featrix"
43
43
  __email__ = "support@featrix.com"
44
44
  __license__ = "MIT"