ml-dash 0.6.7__tar.gz → 0.6.9__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 (37) hide show
  1. {ml_dash-0.6.7 → ml_dash-0.6.9}/PKG-INFO +81 -5
  2. {ml_dash-0.6.7 → ml_dash-0.6.9}/README.md +80 -4
  3. {ml_dash-0.6.7 → ml_dash-0.6.9}/pyproject.toml +1 -1
  4. ml_dash-0.6.9/src/ml_dash/cli_commands/profile.py +227 -0
  5. ml_dash-0.6.7/src/ml_dash/cli_commands/profile.py +0 -92
  6. {ml_dash-0.6.7 → ml_dash-0.6.9}/LICENSE +0 -0
  7. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/__init__.py +0 -0
  8. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/auth/__init__.py +0 -0
  9. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/auth/constants.py +0 -0
  10. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/auth/device_flow.py +0 -0
  11. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/auth/device_secret.py +0 -0
  12. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/auth/exceptions.py +0 -0
  13. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/auth/token_storage.py +0 -0
  14. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/auto_start.py +0 -0
  15. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/buffer.py +0 -0
  16. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/cli.py +0 -0
  17. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/cli_commands/__init__.py +0 -0
  18. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/cli_commands/api.py +0 -0
  19. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/cli_commands/create.py +0 -0
  20. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/cli_commands/download.py +0 -0
  21. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/cli_commands/list.py +0 -0
  22. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/cli_commands/login.py +0 -0
  23. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/cli_commands/logout.py +0 -0
  24. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/cli_commands/upload.py +0 -0
  25. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/client.py +0 -0
  26. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/config.py +0 -0
  27. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/experiment.py +0 -0
  28. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/files.py +0 -0
  29. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/log.py +0 -0
  30. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/metric.py +0 -0
  31. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/params.py +0 -0
  32. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/py.typed +0 -0
  33. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/remote_auto_start.py +0 -0
  34. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/run.py +0 -0
  35. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/snowflake.py +0 -0
  36. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/storage.py +0 -0
  37. {ml_dash-0.6.7 → ml_dash-0.6.9}/src/ml_dash/track.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ml-dash
3
- Version: 0.6.7
3
+ Version: 0.6.9
4
4
  Summary: ML experiment tracking and data storage
5
5
  Keywords: machine-learning,experiment-tracking,mlops,data-storage
6
6
  Author: Ge Yang, Tom Tao
@@ -68,10 +68,11 @@ Description-Content-Type: text/markdown
68
68
 
69
69
  # ML-Dash
70
70
 
71
- A simple and flexible SDK for ML experiment tracking and data storage.
71
+ A simple and flexible SDK for ML experiment tracking and data storage with background buffering for high-performance training.
72
72
 
73
73
  ## Features
74
74
 
75
+ ### Core Features
75
76
  - **Three Usage Styles**: Pre-configured singleton (dxp), context manager, or direct instantiation
76
77
  - **Dual Operation Modes**: Remote (API server) or local (filesystem)
77
78
  - **OAuth2 Authentication**: Secure device flow authentication for CLI and SDK
@@ -82,6 +83,13 @@ A simple and flexible SDK for ML experiment tracking and data storage.
82
83
  - **Rich Metadata**: Tags, bindrs, descriptions, and custom metadata support
83
84
  - **Simple API**: Minimal configuration, maximum flexibility
84
85
 
86
+ ### Performance Features (New in 0.6.7)
87
+ - **Background Buffering**: Non-blocking I/O operations eliminate training interruptions
88
+ - **Automatic Batching**: Time-based (5s) and size-based (100 items) flush triggers
89
+ - **Track API**: Time-series data tracking for robotics, RL, and sequential experiments
90
+ - **Numpy Image Support**: Direct saving of numpy arrays as PNG/JPEG images
91
+ - **Parallel Uploads**: ThreadPoolExecutor for efficient file uploads
92
+
85
93
  ## Installation
86
94
 
87
95
  <table>
@@ -93,14 +101,14 @@ A simple and flexible SDK for ML experiment tracking and data storage.
93
101
  <td>
94
102
 
95
103
  ```bash
96
- uv add ml-dash==0.6.2rc1
104
+ uv add ml-dash
97
105
  ```
98
106
 
99
107
  </td>
100
108
  <td>
101
109
 
102
110
  ```bash
103
- pip install ml-dash==0.6.2rc1
111
+ pip install ml-dash
104
112
  ```
105
113
 
106
114
  </td>
@@ -159,7 +167,75 @@ with Experiment(
159
167
 
160
168
  ```
161
169
 
162
- See [docs/getting-started.md](docs/getting-started.md) for more examples.
170
+ ## New Features in 0.6.7
171
+
172
+ ### 🚀 Background Buffering (Non-blocking I/O)
173
+
174
+ All write operations are now buffered and executed in background threads:
175
+
176
+ ```python
177
+ with Experiment("my-project/exp").run as experiment:
178
+ for i in range(10000):
179
+ # Non-blocking! Returns immediately
180
+ experiment.log(f"Step {i}")
181
+ experiment.metrics("train").log(loss=loss, accuracy=acc)
182
+ experiment.files("frames").save_image(frame, to=f"frame_{i}.jpg")
183
+
184
+ # All data automatically flushed when context exits
185
+ ```
186
+
187
+ Configure buffering via environment variables:
188
+ ```bash
189
+ export ML_DASH_BUFFER_ENABLED=true
190
+ export ML_DASH_FLUSH_INTERVAL=5.0
191
+ export ML_DASH_LOG_BATCH_SIZE=100
192
+ ```
193
+
194
+ ### 📊 Track API (Time-Series Data)
195
+
196
+ Perfect for robotics, RL, and sequential experiments:
197
+
198
+ ```python
199
+ with Experiment("robotics/training").run as experiment:
200
+ for step in range(1000):
201
+ # Track robot position over time
202
+ experiment.track("robot/position").append({
203
+ "step": step,
204
+ "x": position[0],
205
+ "y": position[1],
206
+ "z": position[2]
207
+ })
208
+
209
+ # Track control signals
210
+ experiment.track("robot/control").append({
211
+ "step": step,
212
+ "motor1": ctrl[0],
213
+ "motor2": ctrl[1]
214
+ })
215
+ ```
216
+
217
+ ### 🖼️ Numpy Image Support
218
+
219
+ Save numpy arrays directly as images (PNG/JPEG):
220
+
221
+ ```python
222
+ import numpy as np
223
+
224
+ with Experiment("vision/training").run as experiment:
225
+ # From MuJoCo, OpenCV, PIL, etc.
226
+ pixels = renderer.render() # numpy array
227
+
228
+ # Save as PNG (lossless)
229
+ experiment.files("frames").save_image(pixels, to="frame.png")
230
+
231
+ # Save as JPEG with quality control
232
+ experiment.files("frames").save_image(pixels, to="frame.jpg", quality=85)
233
+
234
+ # Auto-detection also works
235
+ experiment.files("frames").save(pixels, to="frame.jpg")
236
+ ```
237
+
238
+ See [CHANGELOG.md](CHANGELOG.md) for complete release notes.
163
239
 
164
240
  ## Development Setup
165
241
 
@@ -1,9 +1,10 @@
1
1
  # ML-Dash
2
2
 
3
- A simple and flexible SDK for ML experiment tracking and data storage.
3
+ A simple and flexible SDK for ML experiment tracking and data storage with background buffering for high-performance training.
4
4
 
5
5
  ## Features
6
6
 
7
+ ### Core Features
7
8
  - **Three Usage Styles**: Pre-configured singleton (dxp), context manager, or direct instantiation
8
9
  - **Dual Operation Modes**: Remote (API server) or local (filesystem)
9
10
  - **OAuth2 Authentication**: Secure device flow authentication for CLI and SDK
@@ -14,6 +15,13 @@ A simple and flexible SDK for ML experiment tracking and data storage.
14
15
  - **Rich Metadata**: Tags, bindrs, descriptions, and custom metadata support
15
16
  - **Simple API**: Minimal configuration, maximum flexibility
16
17
 
18
+ ### Performance Features (New in 0.6.7)
19
+ - **Background Buffering**: Non-blocking I/O operations eliminate training interruptions
20
+ - **Automatic Batching**: Time-based (5s) and size-based (100 items) flush triggers
21
+ - **Track API**: Time-series data tracking for robotics, RL, and sequential experiments
22
+ - **Numpy Image Support**: Direct saving of numpy arrays as PNG/JPEG images
23
+ - **Parallel Uploads**: ThreadPoolExecutor for efficient file uploads
24
+
17
25
  ## Installation
18
26
 
19
27
  <table>
@@ -25,14 +33,14 @@ A simple and flexible SDK for ML experiment tracking and data storage.
25
33
  <td>
26
34
 
27
35
  ```bash
28
- uv add ml-dash==0.6.2rc1
36
+ uv add ml-dash
29
37
  ```
30
38
 
31
39
  </td>
32
40
  <td>
33
41
 
34
42
  ```bash
35
- pip install ml-dash==0.6.2rc1
43
+ pip install ml-dash
36
44
  ```
37
45
 
38
46
  </td>
@@ -91,7 +99,75 @@ with Experiment(
91
99
 
92
100
  ```
93
101
 
94
- See [docs/getting-started.md](docs/getting-started.md) for more examples.
102
+ ## New Features in 0.6.7
103
+
104
+ ### 🚀 Background Buffering (Non-blocking I/O)
105
+
106
+ All write operations are now buffered and executed in background threads:
107
+
108
+ ```python
109
+ with Experiment("my-project/exp").run as experiment:
110
+ for i in range(10000):
111
+ # Non-blocking! Returns immediately
112
+ experiment.log(f"Step {i}")
113
+ experiment.metrics("train").log(loss=loss, accuracy=acc)
114
+ experiment.files("frames").save_image(frame, to=f"frame_{i}.jpg")
115
+
116
+ # All data automatically flushed when context exits
117
+ ```
118
+
119
+ Configure buffering via environment variables:
120
+ ```bash
121
+ export ML_DASH_BUFFER_ENABLED=true
122
+ export ML_DASH_FLUSH_INTERVAL=5.0
123
+ export ML_DASH_LOG_BATCH_SIZE=100
124
+ ```
125
+
126
+ ### 📊 Track API (Time-Series Data)
127
+
128
+ Perfect for robotics, RL, and sequential experiments:
129
+
130
+ ```python
131
+ with Experiment("robotics/training").run as experiment:
132
+ for step in range(1000):
133
+ # Track robot position over time
134
+ experiment.track("robot/position").append({
135
+ "step": step,
136
+ "x": position[0],
137
+ "y": position[1],
138
+ "z": position[2]
139
+ })
140
+
141
+ # Track control signals
142
+ experiment.track("robot/control").append({
143
+ "step": step,
144
+ "motor1": ctrl[0],
145
+ "motor2": ctrl[1]
146
+ })
147
+ ```
148
+
149
+ ### 🖼️ Numpy Image Support
150
+
151
+ Save numpy arrays directly as images (PNG/JPEG):
152
+
153
+ ```python
154
+ import numpy as np
155
+
156
+ with Experiment("vision/training").run as experiment:
157
+ # From MuJoCo, OpenCV, PIL, etc.
158
+ pixels = renderer.render() # numpy array
159
+
160
+ # Save as PNG (lossless)
161
+ experiment.files("frames").save_image(pixels, to="frame.png")
162
+
163
+ # Save as JPEG with quality control
164
+ experiment.files("frames").save_image(pixels, to="frame.jpg", quality=85)
165
+
166
+ # Auto-detection also works
167
+ experiment.files("frames").save(pixels, to="frame.jpg")
168
+ ```
169
+
170
+ See [CHANGELOG.md](CHANGELOG.md) for complete release notes.
95
171
 
96
172
  ## Development Setup
97
173
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ml-dash"
3
- version = "0.6.7"
3
+ version = "0.6.9"
4
4
  description = "ML experiment tracking and data storage"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -0,0 +1,227 @@
1
+ """Profile command for ml-dash CLI - shows current user and configuration."""
2
+
3
+ import json
4
+ import time
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ from ml_dash.auth.token_storage import decode_jwt_payload, get_token_storage
11
+ from ml_dash.config import config
12
+
13
+
14
+ def add_parser(subparsers):
15
+ """Add profile command parser."""
16
+ parser = subparsers.add_parser(
17
+ "profile",
18
+ help="Show current user profile",
19
+ description="Display the current authenticated user profile and configuration.",
20
+ )
21
+ parser.add_argument(
22
+ "--json",
23
+ action="store_true",
24
+ help="Output as JSON",
25
+ )
26
+ parser.add_argument(
27
+ "--refresh",
28
+ action="store_true",
29
+ help="Fetch fresh profile from server (not from cached token)",
30
+ )
31
+
32
+
33
+ def _fetch_fresh_profile(remote_url: str, token: str) -> dict:
34
+ """Fetch fresh user profile from the API server.
35
+
36
+ Args:
37
+ remote_url: API server URL
38
+ token: JWT authentication token
39
+
40
+ Returns:
41
+ User profile dict with username, email, name, etc.
42
+ """
43
+ try:
44
+ from ml_dash.client import RemoteClient
45
+
46
+ client = RemoteClient(remote_url, api_key=token)
47
+
48
+ # Query for full user profile
49
+ query = """
50
+ query GetUserProfile {
51
+ me {
52
+ id
53
+ username
54
+ name
55
+ email
56
+ }
57
+ }
58
+ """
59
+
60
+ result = client.graphql_query(query)
61
+ me = result.get("me", {})
62
+
63
+ if me:
64
+ return {
65
+ "sub": me.get("id"),
66
+ "username": me.get("username"),
67
+ "name": me.get("name"),
68
+ "email": me.get("email"),
69
+ }
70
+ except Exception as e:
71
+ # If API call fails, return None to fall back to token decoding
72
+ return None
73
+
74
+ return None
75
+
76
+
77
+ def _check_token_expiration(token_payload: dict) -> tuple[bool, str]:
78
+ """Check if token is expired or close to expiring.
79
+
80
+ Args:
81
+ token_payload: Decoded JWT payload
82
+
83
+ Returns:
84
+ Tuple of (is_expired, message)
85
+ """
86
+ exp = token_payload.get("exp")
87
+ if not exp:
88
+ return False, None
89
+
90
+ current_time = int(time.time())
91
+ time_left = exp - current_time
92
+
93
+ if time_left < 0:
94
+ return True, "[red]Token expired[/red]"
95
+ elif time_left < 86400: # Less than 1 day
96
+ hours_left = time_left // 3600
97
+ return False, f"[yellow]Token expires in {hours_left} hours[/yellow]"
98
+ else:
99
+ days_left = time_left // 86400
100
+ return False, f"Expires in {days_left} days"
101
+
102
+ return False, None
103
+
104
+
105
+ def cmd_profile(args) -> int:
106
+ """Execute info command."""
107
+ console = Console()
108
+
109
+ # Load token
110
+ storage = get_token_storage()
111
+ token = storage.load("ml-dash-token")
112
+
113
+ import getpass
114
+
115
+ info = {
116
+ "authenticated": False,
117
+ "remote_url": config.remote_url,
118
+ "local_user": getpass.getuser(),
119
+ }
120
+
121
+ if token:
122
+ info["authenticated"] = True
123
+
124
+ # Decode token payload for initial data and expiration check
125
+ token_payload = decode_jwt_payload(token)
126
+
127
+ # Check token expiration
128
+ is_expired, expiry_message = _check_token_expiration(token_payload)
129
+
130
+ if is_expired:
131
+ info["authenticated"] = False
132
+ info["error"] = "Token expired. Please run 'ml-dash login' to re-authenticate."
133
+ else:
134
+ # Fetch fresh profile from server if requested, or fall back to token
135
+ if args.refresh:
136
+ fresh_profile = _fetch_fresh_profile(config.remote_url, token)
137
+ if fresh_profile:
138
+ info["user"] = fresh_profile
139
+ info["source"] = "server"
140
+ else:
141
+ info["user"] = token_payload
142
+ info["source"] = "token"
143
+ info["warning"] = "Could not fetch fresh profile from server, using cached token data"
144
+ else:
145
+ info["user"] = token_payload
146
+ info["source"] = "token"
147
+
148
+ if expiry_message:
149
+ info["token_status"] = expiry_message
150
+
151
+ if args.json:
152
+ console.print_json(json.dumps(info))
153
+ return 0
154
+
155
+ # Rich display
156
+ if not info["authenticated"]:
157
+ error_msg = info.get("error", "Not authenticated")
158
+ console.print(
159
+ Panel(
160
+ f"[bold cyan]OS Username:[/bold cyan] {info.get('local_user')}\n\n"
161
+ f"[yellow]{error_msg}[/yellow]\n\n"
162
+ "Run [cyan]ml-dash login[/cyan] to authenticate.",
163
+ title="[bold]ML-Dash Info[/bold]",
164
+ border_style="yellow",
165
+ )
166
+ )
167
+ return 0
168
+
169
+ # Build info table
170
+ table = Table(show_header=False, box=None, padding=(0, 2))
171
+ table.add_column("Key", style="bold cyan")
172
+ table.add_column("Value")
173
+
174
+ user = info.get("user", {})
175
+ if user.get("username"):
176
+ table.add_row("Username", user["username"])
177
+ else:
178
+ table.add_row("Username", "[red]Unavailable[/red]")
179
+ if user.get("sub"):
180
+ table.add_row("User ID", user["sub"])
181
+ table.add_row("Name", user.get("name") or "Unknown")
182
+ if user.get("email"):
183
+ table.add_row("Email", user["email"])
184
+ table.add_row("Remote", info.get("remote_url") or "https://api.dash.ml")
185
+
186
+ # Show token status (expiration)
187
+ if info.get("token_status"):
188
+ table.add_row("Token Status", info["token_status"])
189
+
190
+ # Show data source
191
+ source = info.get("source", "token")
192
+ if source == "server":
193
+ table.add_row("Data Source", "[green]Server (Fresh)[/green]")
194
+ else:
195
+ table.add_row("Data Source", "[yellow]Token (Cached)[/yellow]")
196
+
197
+ # Show warning if any
198
+ warning_text = None
199
+ if info.get("warning"):
200
+ warning_text = f"\n[yellow]⚠ {info['warning']}[/yellow]"
201
+
202
+ # Show tip for refreshing
203
+ if source == "token":
204
+ tip_text = "\n[dim]Tip: Use --refresh to fetch fresh data from server[/dim]"
205
+ else:
206
+ tip_text = None
207
+
208
+ # Build panel content
209
+ panel_content = table
210
+ if warning_text or tip_text:
211
+ from rich.console import Group
212
+ items = [table]
213
+ if warning_text:
214
+ items.append(warning_text)
215
+ if tip_text:
216
+ items.append(tip_text)
217
+ panel_content = Group(*items)
218
+
219
+ console.print(
220
+ Panel(
221
+ panel_content,
222
+ title="[bold green]✓ Authenticated[/bold green]",
223
+ border_style="green",
224
+ )
225
+ )
226
+
227
+ return 0
@@ -1,92 +0,0 @@
1
- """Profile command for ml-dash CLI - shows current user and configuration."""
2
-
3
- import json
4
-
5
- from rich.console import Console
6
- from rich.panel import Panel
7
- from rich.table import Table
8
-
9
- from ml_dash.auth.token_storage import decode_jwt_payload, get_token_storage
10
- from ml_dash.config import config
11
-
12
-
13
- def add_parser(subparsers):
14
- """Add profile command parser."""
15
- parser = subparsers.add_parser(
16
- "profile",
17
- help="Show current user profile",
18
- description="Display the current authenticated user profile and configuration.",
19
- )
20
- parser.add_argument(
21
- "--json",
22
- action="store_true",
23
- help="Output as JSON",
24
- )
25
-
26
-
27
- def cmd_profile(args) -> int:
28
- """Execute info command."""
29
- console = Console()
30
-
31
- # Load token
32
- storage = get_token_storage()
33
- token = storage.load("ml-dash-token")
34
-
35
- import getpass
36
-
37
- info = {
38
- "authenticated": False,
39
- "remote_url": config.remote_url,
40
- "local_user": getpass.getuser(),
41
- }
42
-
43
- if token:
44
- info["authenticated"] = True
45
- info["user"] = decode_jwt_payload(token)
46
-
47
- if args.json:
48
- console.print_json(json.dumps(info))
49
- return 0
50
-
51
- # Rich display
52
- if not info["authenticated"]:
53
- console.print(
54
- Panel(
55
- f"[bold cyan]OS Username:[/bold cyan] {info.get('local_user')}\n\n"
56
- "[yellow]Not authenticated[/yellow]\n\n"
57
- "Run [cyan]ml-dash login[/cyan] to authenticate.",
58
- title="[bold]ML-Dash Info[/bold]",
59
- border_style="yellow",
60
- )
61
- )
62
- return 0
63
-
64
- # Build info table
65
- table = Table(show_header=False, box=None, padding=(0, 2))
66
- table.add_column("Key", style="bold cyan")
67
- table.add_column("Value")
68
-
69
- # table.add_row("OS Username", info.get("local_user"))
70
- user = info.get("user", {})
71
- if user.get("username"):
72
- table.add_row("Username", user["username"])
73
- else:
74
- table.add_row("Username", "[red]Unavailable[/red]")
75
- if user.get("sub"):
76
- table.add_row("User ID", user["sub"])
77
- table.add_row("Name", user.get("name") or "Unknown")
78
- if user.get("email"):
79
- table.add_row("Email", user["email"])
80
- table.add_row("Remote", info.get("remote_url") or "https://api.dash.ml")
81
- if info.get("token_expires"):
82
- table.add_row("Token Expires", info["token_expires"])
83
-
84
- console.print(
85
- Panel(
86
- table,
87
- title="[bold green]✓ Authenticated[/bold green]",
88
- border_style="green",
89
- )
90
- )
91
-
92
- return 0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes