File size: 22,539 Bytes
5fe83da
 
 
 
 
 
 
 
 
 
 
14e9cd5
 
5fe83da
 
 
14e9cd5
 
2da5c04
14e9cd5
 
 
 
 
5fe83da
 
 
3c37508
5fe83da
 
3c37508
5fe83da
2da5c04
14e9cd5
 
2da5c04
 
 
c417358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2da5c04
 
c417358
 
 
 
 
14e9cd5
 
c417358
 
 
 
 
2da5c04
 
 
 
 
 
c417358
 
 
 
 
 
 
 
 
40fd629
c417358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5fe83da
14e9cd5
5fe83da
 
 
14e9cd5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5fe83da
 
40fd629
5fe83da
769bb84
5fe83da
 
14e9cd5
5fe83da
 
769bb84
14e9cd5
769bb84
 
 
40fd629
769bb84
 
 
 
5fe83da
 
 
 
 
 
 
 
14e9cd5
5fe83da
 
14e9cd5
 
5fe83da
14e9cd5
 
 
 
 
5fe83da
93ed7a1
 
75bcdb3
93ed7a1
75bcdb3
d47568c
 
14e9cd5
5fe83da
14e9cd5
d47568c
 
 
5fe83da
 
75bcdb3
93ed7a1
14e9cd5
93ed7a1
14e9cd5
 
93ed7a1
3c37508
 
 
 
 
93ed7a1
14e9cd5
5fe83da
93ed7a1
 
14e9cd5
 
 
 
 
93ed7a1
14e9cd5
 
 
 
 
 
 
5fe83da
14e9cd5
 
5fe83da
 
14e9cd5
 
5fe83da
3c37508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14e9cd5
2da5c04
5fe83da
2da5c04
5fe83da
2da5c04
 
 
93ed7a1
2da5c04
14e9cd5
2da5c04
 
 
93c5e53
2da5c04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93c5e53
2da5c04
 
93c5e53
2da5c04
 
 
 
 
 
 
 
14e9cd5
2da5c04
 
 
14e9cd5
2da5c04
5fe83da
75bcdb3
 
2da5c04
75bcdb3
2da5c04
 
 
 
 
75bcdb3
2da5c04
 
 
3c37508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2da5c04
 
 
 
 
 
 
5fe83da
 
2da5c04
 
 
 
 
 
 
eb9e91f
75bcdb3
eb9e91f
3c37508
 
2da5c04
3c37508
2da5c04
 
75bcdb3
2da5c04
 
 
eb9e91f
3c37508
 
 
 
2da5c04
75bcdb3
2da5c04
 
5fe83da
 
 
 
 
 
 
 
c346dad
 
5fe83da
 
14e9cd5
5fe83da
 
 
 
 
 
14e9cd5
5fe83da
 
 
 
 
 
 
 
 
 
 
 
 
 
14e9cd5
 
 
5fe83da
 
2da5c04
14e9cd5
5fe83da
 
2da5c04
 
 
 
 
14e9cd5
 
 
 
 
 
2da5c04
5fe83da
14e9cd5
 
5fe83da
 
 
 
 
 
 
 
 
 
 
 
fd0524b
 
 
 
 
 
 
3c37508
fd0524b
 
 
eb9e91f
fd0524b
 
3c37508
fd0524b
 
 
 
 
 
 
 
3c37508
93c5e53
2da5c04
 
5fe83da
 
93c5e53
 
 
 
 
3c37508
 
93c5e53
2da5c04
3c37508
5fe83da
 
 
 
 
 
 
2da5c04
3c37508
5fe83da
 
2da5c04
 
 
14e9cd5
 
 
 
2da5c04
5fe83da
 
 
14e9cd5
 
 
 
5fe83da
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
#!/usr/bin/env python3
"""
Deployment script for Trackio on Hugging Face Spaces
Automates the process of creating and configuring a Trackio Space
"""

import os
import json
import requests
import subprocess
import sys
import tempfile
import shutil
from pathlib import Path
from typing import Dict, Any, Optional

# Import Hugging Face Hub API
try:
    from huggingface_hub import HfApi, create_repo, upload_file
    HF_HUB_AVAILABLE = True
except ImportError:
    HF_HUB_AVAILABLE = False
    print("Warning: huggingface_hub not available. Install with: pip install huggingface_hub")

class TrackioSpaceDeployer:
    """Deployer for Trackio on Hugging Face Spaces"""
    
    def __init__(self, space_name: str, token: str, git_email: str = None, git_name: str = None, dataset_repo: str = None):
        self.space_name = space_name
        self.token = token
        self.dataset_repo = dataset_repo
        
        # Initialize HF API and get user info
        if HF_HUB_AVAILABLE:
            self.api = HfApi(token=self.token)
            # Get username from token
            try:
                user_info = self.api.whoami()
                # Handle different possible response formats
                if isinstance(user_info, dict):
                    # Try different possible keys for username
                    self.username = (
                        user_info.get('name') or 
                        user_info.get('username') or 
                        user_info.get('user') or 
                        'unknown'
                    )
                elif isinstance(user_info, str):
                    # If whoami returns just the username as string
                    self.username = user_info
                else:
                    # Fallback to CLI method
                    print("⚠️  Unexpected user_info format, trying CLI fallback...")
                    self.username = self._get_username_from_cli()
                
                if self.username and self.username != 'unknown':
                    print(f"βœ… Authenticated as: {self.username}")
                else:
                    print("⚠️  Could not determine username from API, trying CLI...")
                    self.username = self._get_username_from_cli()
                    
            except Exception as e:
                print(f"❌ Failed to get user info from token: {e}")
                print("⚠️  Trying CLI fallback for username...")
                self.username = self._get_username_from_cli()
                if not self.username:
                    print("❌ Could not determine username. Please check your token.")
                    sys.exit(1)
        else:
            self.api = None
            self.username = self._get_username_from_cli()
        
        if not self.username:
            print("❌ Could not determine username. Please check your token.")
            sys.exit(1)
        
        self.space_url = f"https://huggingface.co/spaces/{self.username}/{self.space_name}"
        
        # Git configuration
        self.git_email = git_email or f"{self.username}@huggingface.co"
        self.git_name = git_name or self.username
    
    def _get_username_from_cli(self) -> str:
        """Fallback method to get username using CLI"""
        try:
            # Set HF token for CLI
            os.environ['HF_TOKEN'] = self.token
            
            # Get username using CLI
            result = subprocess.run(
                ["hf", "whoami"],
                capture_output=True,
                text=True,
                timeout=30
            )
            
            if result.returncode == 0:
                username = result.stdout.strip()
                if username:
                    print(f"βœ… Got username from CLI: {username}")
                    return username
                else:
                    print("⚠️  CLI returned empty username")
                    return None
            else:
                print(f"⚠️  CLI whoami failed: {result.stderr}")
                return None
                
        except Exception as e:
            print(f"⚠️  CLI fallback failed: {e}")
            return None
    
    def create_space(self) -> bool:
        """Create a new Hugging Face Space using the latest API"""
        try:
            print(f"Creating Space: {self.space_name}")
            
            if not HF_HUB_AVAILABLE:
                print("❌ huggingface_hub not available, falling back to CLI")
                return self._create_space_cli()
            
            # Use the latest HF Hub API to create space
            repo_id = f"{self.username}/{self.space_name}"
            
            try:
                # Create the space using the API
                create_repo(
                    repo_id=repo_id,
                    token=self.token,
                    repo_type="space",
                    exist_ok=True,
                    private=False,  # Spaces are typically public
                    space_sdk="gradio",  # Specify Gradio SDK
                    space_hardware="cpu-basic"  # Use basic CPU
                )
                
                print(f"βœ… Space created successfully: {self.space_url}")
                return True
                
            except Exception as api_error:
                print(f"API creation failed: {api_error}")
                print("Falling back to CLI method...")
                return self._create_space_cli()
                
        except Exception as e:
            print(f"❌ Error creating space: {e}")
            return False
    
    def _create_space_cli(self) -> bool:
        """Fallback method using CLI commands"""
        try:
            print("Using CLI fallback method...")
            
            # Set HF token for CLI
            os.environ['HF_TOKEN'] = self.token
            
            # Create space using Hugging Face CLI
            cmd = [
                "hf", "repo", "create",
                f"{self.username}/{self.space_name}",
                "--type", "space"
            ]
            
            print(f"Running command: {' '.join(cmd)}")
            result = subprocess.run(cmd, capture_output=True, text=True)
            
            if result.returncode != 0:
                print(f"First attempt failed: {result.stderr}")
                # Try alternative approach without space-specific flags
                print("Retrying with basic space creation...")
                cmd = [
                    "hf", "repo", "create",
                    f"{self.username}/{self.space_name}"
                ]
                result = subprocess.run(cmd, capture_output=True, text=True)
            
            if result.returncode == 0:
                print(f"βœ… Space created successfully: {self.space_url}")
                return True
            else:
                print(f"❌ Failed to create space: {result.stderr}")
                return False
                
        except Exception as e:
            print(f"❌ Error creating space with CLI: {e}")
            return False
    
    def prepare_space_files(self) -> str:
        """Prepare all necessary files for the Space in a temporary directory"""
        try:
            print("Preparing Space files...")
            
            # Create temporary directory
            temp_dir = tempfile.mkdtemp()
            print(f"Created temporary directory: {temp_dir}")
            
            # Get the project root directory (3 levels up from this script)
            project_root = Path(__file__).parent.parent.parent
            templates_dir = project_root / "templates" / "spaces" / "trackio"
            
            # Files to copy from templates/spaces/trackio
            # Include dataset_utils.py and trackio_api_client.py to enable
            # safe dataset upserts and local API client usage on the Space.
            files_to_copy = [
                "app.py",
                "requirements.txt",
                "README.md",
                "dataset_utils.py",
                "trackio_api_client.py",
            ]
            
            # Copy files from templates/spaces/trackio to temp directory
            copied_files = []
            for file_name in files_to_copy:
                source_path = templates_dir / file_name
                dest_path = Path(temp_dir) / file_name
                
                if source_path.exists():
                    # For app.py, we need to customize it with user variables
                    if file_name == "app.py":
                        self._customize_app_py(source_path, dest_path)
                    else:
                        shutil.copy2(source_path, dest_path)
                    copied_files.append(file_name)
                    print(f"βœ… Copied {file_name} to temp directory")
                else:
                    print(f"⚠️  File not found: {source_path}")
            
            # Update README.md with actual space URL
            readme_path = Path(temp_dir) / "README.md"
            if readme_path.exists():
                with open(readme_path, 'r', encoding='utf-8') as f:
                    readme_content = f.read()
                
                # Replace placeholder with actual space URL
                readme_content = readme_content.replace("{SPACE_URL}", self.space_url)
                
                with open(readme_path, 'w', encoding='utf-8') as f:
                    f.write(readme_content)
                
                print(f"βœ… Updated README.md with space URL")
            
            print(f"βœ… Prepared {len(copied_files)} files in temporary directory")
            return temp_dir
            
        except Exception as e:
            print(f"❌ Error preparing files: {e}")
            return None
    
    def _customize_app_py(self, source_path: Path, dest_path: Path):
        """Customize app.py with user-specific variables"""
        try:
            with open(source_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Replace hardcoded values with user-specific ones
            replacements = {
                # Default dataset repository
                "'tonic/trackio-experiments'": f"'{self.dataset_repo or f'{self.username}/trackio-experiments'}'",
                "'trackio-experiments'": f"'{self.dataset_repo or f'{self.username}/trackio-experiments'}" if self.dataset_repo else "'trackio-experiments'",
                
                # Trackio URL
                "'https://tonic-test-trackio-test.hf.space'": f"'{self.space_url}'",
                "'https://your-trackio-space.hf.space'": f"'{self.space_url}'",
                
                # UI default values
                '"tonic/trackio-experiments"': f'"{self.dataset_repo or f"{self.username}/trackio-experiments"}"',
                '"trackio-experiments"': f'"{self.dataset_repo or f"{self.username}/trackio-experiments"}"' if self.dataset_repo else '"trackio-experiments"',
                
                # Examples in help text
                "'tonic/trackio-experiments'": f"'{self.username}/trackio-experiments'",
                "'your-username/trackio-experiments'": f"'{self.username}/trackio-experiments'",
                "'your-username/my-experiments'": f"'{self.username}/my-experiments'"
            }
            
            # Apply replacements
            for old, new in replacements.items():
                content = content.replace(old, new)
            
            # Write customized content
            with open(dest_path, 'w', encoding='utf-8') as f:
                f.write(content)
            
            print(f"βœ… Customized app.py with user variables")
            
        except Exception as e:
            print(f"❌ Error customizing app.py: {e}")
            # Fallback to copying original file
            shutil.copy2(source_path, dest_path)
    
    def upload_files_to_space(self, temp_dir: str) -> bool:
        """Upload files to the Space using HF Hub API directly"""
        try:
            print("Uploading files to Space using HF Hub API...")
            
            if not HF_HUB_AVAILABLE:
                print("❌ huggingface_hub not available for file upload")
                return False
            
            repo_id = f"{self.username}/{self.space_name}"
            
            # Upload each file using the HF Hub API
            temp_path = Path(temp_dir)
            uploaded_files = []
            
            for file_path in temp_path.iterdir():
                if file_path.is_file():
                    try:
                        # Upload file to the space
                        upload_file(
                            path_or_fileobj=str(file_path),
                            path_in_repo=file_path.name,
                            repo_id=repo_id,
                            repo_type="space",
                            token=self.token
                        )
                        uploaded_files.append(file_path.name)
                        print(f"βœ… Uploaded {file_path.name}")
                    except Exception as e:
                        print(f"❌ Failed to upload {file_path.name}: {e}")
                        return False
            
            print(f"βœ… Successfully uploaded {len(uploaded_files)} files to Space")
            return True
            
        except Exception as e:
            print(f"❌ Error uploading files: {e}")
            return False
    
    def set_space_secrets(self) -> bool:
        """Set environment variables/secrets for the Space using HF Hub API"""
        try:
            print("Setting Space secrets using HF Hub API...")
            
            if not HF_HUB_AVAILABLE:
                print("❌ huggingface_hub not available for setting secrets")
                return self._manual_secret_setup()
            
            repo_id = f"{self.username}/{self.space_name}"
            
            # Use the provided token as HF_TOKEN (starts as write token, will be switched to read token later)
            hf_token = self.token
            
            # Set the HF_TOKEN secret for the space using the API
            try:
                self.api.add_space_secret(
                    repo_id=repo_id,
                    key="HF_TOKEN",
                    value=hf_token,
                    description="Hugging Face token for dataset access (starts as write, switches to read)"
                )
                print("βœ… Successfully set HF_TOKEN secret via API")
                
                # Set the TRACKIO_DATASET_REPO variable
                dataset_repo = self.dataset_repo or f"{self.username}/trackio-experiments"
                self.api.add_space_variable(
                    repo_id=repo_id,
                    key="TRACKIO_DATASET_REPO",
                    value=dataset_repo,
                    description="Dataset repository for Trackio experiments"
                )
                print(f"βœ… Successfully set TRACKIO_DATASET_REPO variable: {dataset_repo}")
                
                # Set the TRACKIO_URL variable
                self.api.add_space_variable(
                    repo_id=repo_id,
                    key="TRACKIO_URL",
                    value=self.space_url,
                    description="Trackio Space URL for monitoring"
                )
                print(f"βœ… Successfully set TRACKIO_URL variable: {self.space_url}")
                
                return True
                
            except Exception as api_error:
                print(f"❌ Failed to set secrets via API: {api_error}")
                print("Falling back to manual setup...")
                return self._manual_secret_setup()
            
        except Exception as e:
            print(f"❌ Error setting space secrets: {e}")
            return self._manual_secret_setup()
    
    def _manual_secret_setup(self) -> bool:
        """Fallback method for manual secret setup"""
        print("πŸ“ Manual Space Secrets Configuration:")
        
        # Use the provided token as HF_TOKEN, but never display it
        hf_token = self.token
        print(f"   HF_TOKEN={'*' * 10}...hidden")
        
        dataset_repo = self.dataset_repo or f"{self.username}/trackio-experiments"
        print(f"   TRACKIO_DATASET_REPO={dataset_repo}")
        print(f"   TRACKIO_URL={self.space_url}")
        
        print("\nπŸ”§ To set secrets in your Space:")
        print(f"1. Go to your Space settings: {self.space_url}/settings")
        print("2. Navigate to the 'Repository secrets' section")
        print("3. Add the following secrets:")
        print(f"   Name: HF_TOKEN")
        print(f"   Value: <your token>")
        print(f"   Name: TRACKIO_DATASET_REPO")
        print(f"   Value: {dataset_repo}")
        print(f"   Name: TRACKIO_URL")
        print(f"   Value: {self.space_url}")
        print("4. Save the secrets")
        print("\nNote: HF_TOKEN starts as write token and will be switched to read token after training")
        
        return True
    
    def test_space(self) -> bool:
        """Test if the Space is working correctly"""
        try:
            print("Testing Space...")
            
            # Wait a bit for the space to build
            import time
            print("Waiting 120 seconds for Space to build...")
            time.sleep(120)
            
            # Try to access the space
            response = requests.get(self.space_url, timeout=30)
            
            if response.status_code == 200:
                print(f"βœ… Space is accessible: {self.space_url}")
                return True
            else:
                print(f"⚠️  Space returned status code: {response.status_code}")
                print(f"Response: {response.text[:500]}...")
                return False
                
        except Exception as e:
            print(f"❌ Error testing space: {e}")
            return False
    
    def deploy(self) -> bool:
        """Complete deployment process"""
        print("πŸš€ Starting Trackio Space deployment...")
        
        # Step 1: Create space
        if not self.create_space():
            return False
        
        # Step 2: Prepare files
        temp_dir = self.prepare_space_files()
        if not temp_dir:
            return False
        
        # Step 3: Upload files using HF Hub API
        if not self.upload_files_to_space(temp_dir):
            return False
        
        # Step 4: Set space secrets using API
        if not self.set_space_secrets():
            return False
        
        # Step 5: Clean up temp directory
        try:
            shutil.rmtree(temp_dir)
            print("βœ… Cleaned up temporary directory")
        except Exception as e:
            print(f"⚠️  Warning: Could not clean up temp directory: {e}")
        
        # Step 6: Test space
        if not self.test_space():
            print("⚠️  Space created but may need more time to build")
            print("Please check the Space manually in a few minutes")
        
        print(f"πŸŽ‰ Deployment completed!")
        print(f"πŸ“Š Trackio Space URL: {self.space_url}")
        print(f"πŸ”§ Space configuration: {self.space_url}/settings")
        
        return True

def main():
    """Main deployment function"""
    print("Trackio Space Deployment Script")
    print("=" * 40)
    
    # Check if arguments are provided
    if len(sys.argv) >= 3:
        # Use command line arguments
        space_name = sys.argv[1]
        token = sys.argv[2]
        git_email = sys.argv[3] if len(sys.argv) > 3 else None
        git_name = sys.argv[4] if len(sys.argv) > 4 else None
        dataset_repo = sys.argv[5] if len(sys.argv) > 5 else None
        
        print(f"Using provided arguments:")
        print(f"  Space name: {space_name}")
        print(f"  Token: <hidden>")
        print(f"  Git email: {git_email or 'default'}")
        print(f"  Git name: {git_name or 'default'}")
        print(f"  Dataset repo: {dataset_repo or 'default'}")
    else:
        # Get user input (no username needed - will be extracted from token)
        space_name = input("Enter Space name (e.g., trackio-monitoring): ").strip()
        token = input("Enter your Hugging Face token: ").strip()
        
        # Get git configuration (optional)
        git_email = input("Enter your git email (optional, press Enter for default): ").strip()
        git_name = input("Enter your git name (optional, press Enter for default): ").strip()
        dataset_repo = input("Enter dataset repository (optional, press Enter for default): ").strip()
    
    if not space_name or not token:
        print("❌ Space name and token are required")
        sys.exit(1)
    
    # Use empty strings if not provided
    if not git_email:
        git_email = None
    if not git_name:
        git_name = None
    if not dataset_repo:
        dataset_repo = None
    
    # Create deployer (username will be extracted from token)
    deployer = TrackioSpaceDeployer(space_name, token, git_email, git_name, dataset_repo)
    
    # Run deployment
    success = deployer.deploy()
    
    if success:
        print("\nβœ… Deployment successful!")
        print(f"🌐 Your Trackio Space: {deployer.space_url}")
        print(f"πŸ‘€ Username: {deployer.username}")
        print(f"πŸ“Š Dataset Repository: {deployer.dataset_repo or f'{deployer.username}/trackio-experiments'}")
        print("\nNext steps:")
        print("1. Wait for the Space to build (usually 2-5 minutes)")
        print("2. Secrets have been automatically set via API")
        print("3. Test the interface by visiting the Space URL")
        print("4. Use the Space URL in your training scripts")
        print("\nIf the Space doesn't work immediately, check:")
        print("- The Space logs at the Space URL")
        print("- That all files were uploaded correctly")
        print("- That the HF token has write permissions")
        print("- That the secrets were set correctly in Space settings")
    else:
        print("\n❌ Deployment failed!")
        print("Check the error messages above and try again.")
        print("\nTroubleshooting:")
        print("1. Verify your HF token has write permissions")
        print("2. Check that the space name is available")
        print("3. Try creating the space manually on HF first")

if __name__ == "__main__":
    main()