machineconfig 3.85__py3-none-any.whl → 3.88__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of machineconfig might be problematic. Click here for more details.

@@ -151,10 +151,10 @@ manager.run_monitoring_routine()
151
151
 
152
152
 
153
153
  def split_too_many_tabs_to_run_in_sequential_sessions(layout_tabs: list[TabConfig], every: int):
154
- from machineconfig.utils.accessories import split
154
+ from machineconfig.utils.accessories import split_list
155
155
  from machineconfig.cluster.sessions_managers.zellij_local_manager import ZellijLocalManager
156
156
 
157
- for idx, layout_tabs_chunk in enumerate(split(layout_tabs, every=every)):
157
+ for idx, layout_tabs_chunk in enumerate(split_list(layout_tabs, every=every, to=None)):
158
158
  a_layout_file: LayoutConfig = {"layoutName": f"split_{idx}", "layoutTabs": layout_tabs_chunk}
159
159
  manager = ZellijLocalManager(session_layouts=[a_layout_file])
160
160
  manager.start_all_sessions(poll_interval=2, poll_seconds=2)
@@ -163,10 +163,9 @@ def split_too_many_tabs_to_run_in_sequential_sessions(layout_tabs: list[TabConfi
163
163
 
164
164
 
165
165
  def split_too_many_layouts_to_run_in_sequential_sessions(layouts: list[LayoutConfig], every: int):
166
- from machineconfig.utils.accessories import split
166
+ from machineconfig.utils.accessories import split_list
167
167
  from machineconfig.cluster.sessions_managers.zellij_local_manager import ZellijLocalManager
168
-
169
- for _idx, layout_chunk in enumerate(split(layouts, every=every)):
168
+ for _idx, layout_chunk in enumerate(split_list(layouts, every=every)):
170
169
  manager = ZellijLocalManager(session_layouts=layout_chunk)
171
170
  manager.start_all_sessions(poll_interval=2, poll_seconds=2)
172
171
  manager.run_monitoring_routine(wait_ms=2000)
@@ -10,34 +10,39 @@ if TYPE_CHECKING:
10
10
  from machineconfig.scripts.python.fire_jobs_args_helper import FireJobArgs
11
11
 
12
12
 
13
- def select_layout(layouts_json_file: Path, layout_name: Optional[str]):
13
+ def select_layout(layouts_json_file: Path, layouts_name: Optional[list[str]]) -> list[LayoutConfig]:
14
14
  import json
15
-
16
15
  layout_file: LayoutsFile = json.loads(layouts_json_file.read_text(encoding="utf-8"))
17
16
  if len(layout_file["layouts"]) == 0:
18
17
  raise ValueError(f"No layouts found in {layouts_json_file}")
19
- if layout_name is None:
18
+ if layouts_name is None:
20
19
  options = [layout["layoutName"] for layout in layout_file["layouts"]]
21
20
  from machineconfig.utils.options import choose_from_options
22
-
23
- layout_name = choose_from_options(multi=False, options=options, prompt="Choose a layout configuration:", fzf=True, msg="Choose one option")
24
- print(f"Selected layout: {layout_name}")
25
- layout_chosen = next((layout for layout in layout_file["layouts"] if layout["layoutName"] == layout_name), None)
26
- if layout_chosen is None:
27
- layout_chosen = next((layout for layout in layout_file["layouts"] if layout["layoutName"].lower() == layout_name.lower()), None)
28
- if layout_chosen is None:
29
- available_layouts = [layout["layoutName"] for layout in layout_file["layouts"]]
30
- raise ValueError(f"Layout '{layout_name}' not found. Available layouts: {available_layouts}")
31
- return layout_chosen
21
+ layouts_name = choose_from_options(multi=True, options=options, prompt="Choose a layout configuration:", fzf=True, msg="Choose one option")
22
+ print(f"Selected layout(s): {layouts_name}")
23
+ # layout_chosen = next((layout for layout in layout_file["layouts"] if layout["layoutName"] == layouts_name), None)
24
+ # if layout_chosen is None:
25
+ # layout_chosen = next((layout for layout in layout_file["layouts"] if layout["layoutName"].lower() == layouts_name.lower()), None)
26
+ # if layout_chosen is None:
27
+ # available_layouts = [layout["layoutName"] for layout in layout_file["layouts"]]
28
+ # raise ValueError(f"Layout '{layouts_name}' not found. Available layouts: {available_layouts}")
29
+ layouts_chosen: list[LayoutConfig] = []
30
+ for name in layouts_name:
31
+ layout_chosen = next((layout for layout in layout_file["layouts"] if layout["layoutName"] == name), None)
32
+ if layout_chosen is None:
33
+ layout_chosen = next((layout for layout in layout_file["layouts"] if layout["layoutName"].lower() == name.lower()), None)
34
+ if layout_chosen is None:
35
+ available_layouts = [layout["layoutName"] for layout in layout_file["layouts"]]
36
+ raise ValueError(f"Layout '{name}' not found. Available layouts: {available_layouts}")
37
+ layouts_chosen.append(layout_chosen)
38
+ return layouts_chosen
32
39
 
33
40
 
34
41
  def launch_layout(layout_config: LayoutConfig) -> Optional[Exception]:
35
42
  import platform
36
-
37
43
  if platform.system() == "Linux" or platform.system() == "Darwin":
38
44
  print("🧑‍💻 Launching layout using Zellij terminal multiplexer...")
39
45
  from machineconfig.cluster.sessions_managers.zellij_local import run_zellij_layout
40
-
41
46
  run_zellij_layout(layout_config=layout_config)
42
47
  elif platform.system() == "Windows":
43
48
  print("🧑‍💻 Launching layout using Windows Terminal...")
@@ -63,4 +68,7 @@ def handle_layout_args(args: "FireJobArgs") -> None:
63
68
  choice_file = PathExtended(choice_file)
64
69
  else:
65
70
  choice_file = path_obj
66
- launch_layout(layout_config=select_layout(layouts_json_file=choice_file, layout_name=args.function))
71
+ if args.function is None: layouts_name = None
72
+ else: layouts_name = args.function.split(",")
73
+ for a_layout_config in select_layout(layouts_json_file=choice_file, layouts_name=layouts_name):
74
+ launch_layout(layout_config=a_layout_config)
@@ -8,80 +8,13 @@ in the event that username@github.com is not mentioned in the remote url.
8
8
  from machineconfig.utils.io import read_ini
9
9
  from machineconfig.utils.source_of_truth import CONFIG_PATH, DEFAULTS_PATH
10
10
  from machineconfig.utils.path_extended import PathExtended as PathExtended
11
- from machineconfig.utils.accessories import randstr
12
- from machineconfig.scripts.python.repos_helper_update import update_repository
13
11
  from machineconfig.scripts.python.repos_helper_record import main as record_repos
14
12
  from machineconfig.scripts.python.repos_helper_clone import clone_repos
13
+ from machineconfig.scripts.python.repos_helper_action import perform_git_operations
15
14
 
16
15
  import typer
17
- from enum import Enum
18
16
  from typing import Annotated, Optional
19
17
 
20
- from rich import print as pprint
21
-
22
-
23
- class GitAction(Enum):
24
- commit = "commit"
25
- push = "push"
26
- pull = "pull"
27
-
28
-
29
- def git_action(path: PathExtended, action: GitAction, mess: Optional[str] = None, r: bool = False, auto_sync: bool = True) -> bool:
30
- """Perform git actions using Python instead of shell scripts. Returns True if successful."""
31
- from git.exc import InvalidGitRepositoryError
32
- from git.repo import Repo
33
-
34
- try:
35
- repo = Repo(str(path), search_parent_directories=False)
36
- except InvalidGitRepositoryError:
37
- pprint(f"⚠️ Skipping {path} because it is not a git repository.")
38
- if r:
39
- results = [git_action(path=sub_path, action=action, mess=mess, r=r, auto_sync=auto_sync) for sub_path in path.search()]
40
- return all(results) # Return True only if all recursive operations succeeded
41
- else:
42
- return False
43
-
44
- print(f">>>>>>>>> 🔧{action} - {path}")
45
-
46
- try:
47
- if action == GitAction.commit:
48
- if mess is None:
49
- mess = "auto_commit_" + randstr()
50
-
51
- # Check if there are changes to commit
52
- if repo.is_dirty() or repo.untracked_files:
53
- repo.git.add(A=True) # Stage all changes
54
- repo.index.commit(mess)
55
- print(f"✅ Committed changes with message: {mess}")
56
- return True
57
- else:
58
- print("ℹ️ No changes to commit")
59
- return True
60
-
61
- elif action == GitAction.push:
62
- success = True
63
- for remote in repo.remotes:
64
- try:
65
- print(f"🚀 Pushing to {remote.url}")
66
- remote.push(repo.active_branch.name)
67
- print(f"✅ Pushed to {remote.name}")
68
- except Exception as e:
69
- print(f"❌ Failed to push to {remote.name}: {e}")
70
- success = False
71
- return success
72
-
73
- elif action == GitAction.pull:
74
- # Use the enhanced update function with uv sync support
75
- update_repository(repo, auto_sync=auto_sync, allow_password_prompt=False)
76
- print("✅ Pull completed")
77
- return True
78
-
79
- except Exception as e:
80
- print(f"❌ Error performing {action} on {path}: {e}")
81
- return False
82
-
83
- return True
84
-
85
18
 
86
19
  def main(
87
20
  directory: Annotated[str, typer.Argument(help="📁 Folder containing repos to record or a specs JSON file to follow.")] = "",
@@ -105,9 +38,7 @@ def main(
105
38
  repos_root = PathExtended.home().joinpath("code") # it is a positional argument, can never be empty.
106
39
  else:
107
40
  repos_root = PathExtended(directory).expanduser().absolute()
108
-
109
41
  auto_sync = not no_sync # Enable auto sync by default, disable with --no-sync
110
-
111
42
  if record:
112
43
  save_path = record_repos(repos_root=repos_root)
113
44
  if cloud is not None:
@@ -115,7 +46,6 @@ def main(
115
46
 
116
47
  elif clone or checkout or checkout_to_branch:
117
48
  print("\n📥 Cloning or checking out repositories...")
118
- print(">>>>>>>>> Cloning Repos")
119
49
  if not repos_root.exists() or repos_root.name != "repos.json":
120
50
  repos_root = PathExtended(CONFIG_PATH).joinpath("repos").joinpath(repos_root.rel2home()).joinpath("repos.json")
121
51
  if not repos_root.exists():
@@ -130,23 +60,15 @@ def main(
130
60
  clone_repos(spec_path=repos_root, preferred_remote=None, checkout_branch_flag=checkout_to_branch, checkout_commit_flag=checkout)
131
61
 
132
62
  elif all or commit or pull or push:
133
- print(f"\n🔄 Performing Git actions on repositories @ `{repos_root}`...")
134
- overall_success = True
135
- for a_path in repos_root.search("*"):
136
- print(f"{('Handling ' + str(a_path)).center(80, '-')}")
137
- path_success = True
138
- if pull or all:
139
- path_success = git_action(path=a_path, action=GitAction.pull, r=recursive, auto_sync=auto_sync) and path_success
140
- if commit or all:
141
- path_success = git_action(a_path, action=GitAction.commit, r=recursive, auto_sync=auto_sync) and path_success
142
- if push or all:
143
- path_success = git_action(a_path, action=GitAction.push, r=recursive, auto_sync=auto_sync) and path_success
144
- overall_success = overall_success and path_success
145
-
146
- if overall_success:
147
- print("✅ All git operations completed successfully")
148
- else:
149
- print("⚠️ Some git operations encountered issues")
63
+ # Use the new helper function for git operations
64
+ perform_git_operations(
65
+ repos_root=repos_root,
66
+ pull=pull or all,
67
+ commit=commit or all,
68
+ push=push or all,
69
+ recursive=recursive,
70
+ auto_sync=auto_sync
71
+ )
150
72
  else:
151
73
  print("❌ No action specified. Try passing --push, --pull, --commit, or --all.")
152
74
 
@@ -0,0 +1,335 @@
1
+ from machineconfig.utils.path_extended import PathExtended as PathExtended
2
+ from machineconfig.utils.accessories import randstr
3
+ from machineconfig.scripts.python.repos_helper_update import update_repository
4
+
5
+ from typing import Optional
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+
9
+ from rich import print as pprint
10
+
11
+
12
+ class GitAction(Enum):
13
+ commit = "commit"
14
+ push = "push"
15
+ pull = "pull"
16
+
17
+
18
+ @dataclass
19
+ class GitOperationResult:
20
+ """Result of a git operation on a single repository."""
21
+ repo_path: PathExtended
22
+ action: str
23
+ success: bool
24
+ message: str
25
+ is_git_repo: bool = True
26
+ had_changes: bool = False
27
+ remote_count: int = 0
28
+
29
+
30
+ @dataclass
31
+ class GitOperationSummary:
32
+ """Summary of all git operations performed."""
33
+ # Basic statistics
34
+ total_paths_processed: int = 0
35
+ git_repos_found: int = 0
36
+ non_git_paths: int = 0
37
+
38
+ # Per-operation statistics
39
+ commits_attempted: int = 0
40
+ commits_successful: int = 0
41
+ commits_no_changes: int = 0
42
+ commits_failed: int = 0
43
+
44
+ pulls_attempted: int = 0
45
+ pulls_successful: int = 0
46
+ pulls_failed: int = 0
47
+
48
+ pushes_attempted: int = 0
49
+ pushes_successful: int = 0
50
+ pushes_failed: int = 0
51
+
52
+ def __post_init__(self):
53
+ self.failed_operations: list[GitOperationResult] = []
54
+ self.repos_without_remotes: list[PathExtended] = []
55
+
56
+
57
+ def git_action(path: PathExtended, action: GitAction, mess: Optional[str] = None, r: bool = False, auto_sync: bool = True) -> GitOperationResult:
58
+ """Perform git actions using Python instead of shell scripts. Returns detailed operation result."""
59
+ from git.exc import InvalidGitRepositoryError
60
+ from git.repo import Repo
61
+
62
+ try:
63
+ repo = Repo(str(path), search_parent_directories=False)
64
+ except InvalidGitRepositoryError:
65
+ pprint(f"⚠️ Skipping {path} because it is not a git repository.")
66
+ if r:
67
+ results = [git_action(path=sub_path, action=action, mess=mess, r=r, auto_sync=auto_sync) for sub_path in path.search()]
68
+ # For recursive calls, we need to aggregate results somehow
69
+ # For now, return success if all recursive operations succeeded
70
+ all_successful = all(result.success for result in results)
71
+ return GitOperationResult(
72
+ repo_path=path,
73
+ action=action.value,
74
+ success=all_successful,
75
+ message=f"Recursive operation: {len([r for r in results if r.success])}/{len(results)} succeeded",
76
+ is_git_repo=False
77
+ )
78
+ else:
79
+ return GitOperationResult(
80
+ repo_path=path,
81
+ action=action.value,
82
+ success=False,
83
+ message="Not a git repository",
84
+ is_git_repo=False
85
+ )
86
+
87
+ print(f">>>>>>>>> 🔧{action} - {path}")
88
+ remote_count = len(repo.remotes)
89
+
90
+ try:
91
+ if action == GitAction.commit:
92
+ if mess is None:
93
+ mess = "auto_commit_" + randstr()
94
+
95
+ # Check if there are changes to commit
96
+ if repo.is_dirty() or repo.untracked_files:
97
+ repo.git.add(A=True) # Stage all changes
98
+ repo.index.commit(mess)
99
+ print(f"✅ Committed changes with message: {mess}")
100
+ return GitOperationResult(
101
+ repo_path=path,
102
+ action=action.value,
103
+ success=True,
104
+ message=f"Committed changes with message: {mess}",
105
+ had_changes=True,
106
+ remote_count=remote_count
107
+ )
108
+ else:
109
+ print("ℹ️ No changes to commit")
110
+ return GitOperationResult(
111
+ repo_path=path,
112
+ action=action.value,
113
+ success=True,
114
+ message="No changes to commit",
115
+ had_changes=False,
116
+ remote_count=remote_count
117
+ )
118
+
119
+ elif action == GitAction.push:
120
+ if not repo.remotes:
121
+ print("⚠️ No remotes configured for push")
122
+ return GitOperationResult(
123
+ repo_path=path,
124
+ action=action.value,
125
+ success=False,
126
+ message="No remotes configured",
127
+ remote_count=0
128
+ )
129
+
130
+ success = True
131
+ failed_remotes = []
132
+ for remote in repo.remotes:
133
+ try:
134
+ print(f"🚀 Pushing to {remote.url}")
135
+ remote.push(repo.active_branch.name)
136
+ print(f"✅ Pushed to {remote.name}")
137
+ except Exception as e:
138
+ print(f"❌ Failed to push to {remote.name}: {e}")
139
+ failed_remotes.append(f"{remote.name}: {str(e)}")
140
+ success = False
141
+
142
+ message = "Push successful" if success else f"Push failed for: {', '.join(failed_remotes)}"
143
+ return GitOperationResult(
144
+ repo_path=path,
145
+ action=action.value,
146
+ success=success,
147
+ message=message,
148
+ remote_count=remote_count
149
+ )
150
+
151
+ elif action == GitAction.pull:
152
+ # Use the enhanced update function with uv sync support
153
+ try:
154
+ update_repository(repo, auto_sync=auto_sync, allow_password_prompt=False)
155
+ print("✅ Pull completed")
156
+ return GitOperationResult(
157
+ repo_path=path,
158
+ action=action.value,
159
+ success=True,
160
+ message="Pull completed successfully",
161
+ remote_count=remote_count
162
+ )
163
+ except Exception as e:
164
+ print(f"❌ Pull failed: {e}")
165
+ return GitOperationResult(
166
+ repo_path=path,
167
+ action=action.value,
168
+ success=False,
169
+ message=f"Pull failed: {str(e)}",
170
+ remote_count=remote_count
171
+ )
172
+
173
+ except Exception as e:
174
+ print(f"❌ Error performing {action} on {path}: {e}")
175
+ return GitOperationResult(
176
+ repo_path=path,
177
+ action=action.value,
178
+ success=False,
179
+ message=f"Error: {str(e)}",
180
+ remote_count=remote_count
181
+ )
182
+
183
+ # This should never be reached, but just in case
184
+ return GitOperationResult(
185
+ repo_path=path,
186
+ action=action.value,
187
+ success=False,
188
+ message="Unknown error",
189
+ remote_count=remote_count
190
+ )
191
+
192
+
193
+ def print_git_operations_summary(summary: GitOperationSummary, operations_performed: list[str]) -> None:
194
+ """Print a detailed summary of git operations similar to repos_helper_record.py."""
195
+ print("\n📊 Git Operations Summary:")
196
+ print(f" Total paths processed: {summary.total_paths_processed}")
197
+ print(f" Git repositories found: {summary.git_repos_found}")
198
+ print(f" Non-git paths skipped: {summary.non_git_paths}")
199
+
200
+ # Show per-operation statistics
201
+ if "commit" in operations_performed:
202
+ print("\n💾 Commit Operations:")
203
+ print(f" Attempted: {summary.commits_attempted}")
204
+ print(f" Successful: {summary.commits_successful}")
205
+ print(f" No changes: {summary.commits_no_changes}")
206
+ print(f" Failed: {summary.commits_failed}")
207
+
208
+ if "pull" in operations_performed:
209
+ print("\n⬇️ Pull Operations:")
210
+ print(f" Attempted: {summary.pulls_attempted}")
211
+ print(f" Successful: {summary.pulls_successful}")
212
+ print(f" Failed: {summary.pulls_failed}")
213
+
214
+ if "push" in operations_performed:
215
+ print("\n🚀 Push Operations:")
216
+ print(f" Attempted: {summary.pushes_attempted}")
217
+ print(f" Successful: {summary.pushes_successful}")
218
+ print(f" Failed: {summary.pushes_failed}")
219
+
220
+ # Show repositories without remotes (important for push operations)
221
+ if summary.repos_without_remotes:
222
+ print(f"\n⚠️ WARNING: {len(summary.repos_without_remotes)} repositories have no remote configurations:")
223
+ for repo_path in summary.repos_without_remotes:
224
+ print(f" • {repo_path.name} ({repo_path})")
225
+ print(" These repositories cannot be pushed to remote servers.")
226
+ else:
227
+ if "push" in operations_performed:
228
+ print("\n✅ All repositories have remote configurations.")
229
+
230
+ # Show failed operations
231
+ if summary.failed_operations:
232
+ print(f"\n❌ FAILED OPERATIONS ({len(summary.failed_operations)} total):")
233
+
234
+ # Group failed operations by type
235
+ failed_by_action = {}
236
+ for failed_op in summary.failed_operations:
237
+ if failed_op.action not in failed_by_action:
238
+ failed_by_action[failed_op.action] = []
239
+ failed_by_action[failed_op.action].append(failed_op)
240
+
241
+ for action, failures in failed_by_action.items():
242
+ print(f"\n {action.upper()} failures ({len(failures)}):")
243
+ for failure in failures:
244
+ if not failure.is_git_repo:
245
+ print(f" • {failure.repo_path.name} ({failure.repo_path}) - Not a git repository")
246
+ else:
247
+ print(f" • {failure.repo_path.name} ({failure.repo_path}) - {failure.message}")
248
+ else:
249
+ print("\n✅ All git operations completed successfully!")
250
+
251
+ # Overall success assessment
252
+ total_failed = len(summary.failed_operations)
253
+ total_operations = (summary.commits_attempted + summary.pulls_attempted +
254
+ summary.pushes_attempted)
255
+
256
+ if total_failed == 0 and total_operations > 0:
257
+ print(f"\n🎉 SUCCESS: All {total_operations} operations completed successfully!")
258
+ elif total_operations == 0:
259
+ print("\n📝 No git operations were performed.")
260
+ else:
261
+ success_rate = ((total_operations - total_failed) / total_operations * 100) if total_operations > 0 else 0
262
+ print(f"\n⚖️ SUMMARY: {total_operations - total_failed}/{total_operations} operations succeeded ({success_rate:.1f}% success rate)")
263
+ if total_failed > 0:
264
+ print(" Review the failed operations above for details on what needs attention.")
265
+
266
+
267
+ def perform_git_operations(repos_root: PathExtended, pull: bool, commit: bool, push: bool, recursive: bool, auto_sync: bool) -> None:
268
+ """Perform git operations on all repositories and provide detailed summary."""
269
+ print(f"\n🔄 Performing Git actions on repositories @ `{repos_root}`...")
270
+
271
+ # Initialize summary tracking
272
+ summary = GitOperationSummary()
273
+ operations_performed = []
274
+
275
+ # Determine which operations to perform
276
+ if pull:
277
+ operations_performed.append("pull")
278
+ if commit:
279
+ operations_performed.append("commit")
280
+ if push:
281
+ operations_performed.append("push")
282
+
283
+ for a_path in repos_root.search("*"):
284
+ print(f"{('Handling ' + str(a_path)).center(80, '-')}")
285
+ summary.total_paths_processed += 1
286
+
287
+ # Check if this is a git repository first
288
+ from git.exc import InvalidGitRepositoryError
289
+ from git.repo import Repo
290
+
291
+ try:
292
+ repo = Repo(str(a_path), search_parent_directories=False)
293
+ summary.git_repos_found += 1
294
+
295
+ # Track repos without remotes
296
+ if len(repo.remotes) == 0:
297
+ summary.repos_without_remotes.append(a_path)
298
+
299
+ # Now perform the actual operations
300
+ if pull:
301
+ result = git_action(path=a_path, action=GitAction.pull, r=recursive, auto_sync=auto_sync)
302
+ summary.pulls_attempted += 1
303
+ if result.success:
304
+ summary.pulls_successful += 1
305
+ else:
306
+ summary.pulls_failed += 1
307
+ summary.failed_operations.append(result)
308
+
309
+ if commit:
310
+ result = git_action(a_path, action=GitAction.commit, r=recursive, auto_sync=auto_sync)
311
+ summary.commits_attempted += 1
312
+ if result.success:
313
+ if result.had_changes:
314
+ summary.commits_successful += 1
315
+ else:
316
+ summary.commits_no_changes += 1
317
+ else:
318
+ summary.commits_failed += 1
319
+ summary.failed_operations.append(result)
320
+
321
+ if push:
322
+ result = git_action(a_path, action=GitAction.push, r=recursive, auto_sync=auto_sync)
323
+ summary.pushes_attempted += 1
324
+ if result.success:
325
+ summary.pushes_successful += 1
326
+ else:
327
+ summary.pushes_failed += 1
328
+ summary.failed_operations.append(result)
329
+
330
+ except InvalidGitRepositoryError:
331
+ summary.non_git_paths += 1
332
+ pprint(f"⚠️ Skipping {a_path} because it is not a git repository.")
333
+
334
+ # Print the detailed summary
335
+ print_git_operations_summary(summary, operations_performed)
@@ -1,11 +1,11 @@
1
1
 
2
2
  import typer
3
-
3
+ from typing import Annotated
4
4
 
5
5
  def func(name: str):
6
6
  print(f"Hello, {name}! from func")
7
7
 
8
- def hello(name: str):
8
+ def hello(*, name: Annotated[str, typer.Option(..., help="Name to greet")]):
9
9
  print(f"Hello, {name}!")
10
10
 
11
11
  def main():
@@ -13,4 +13,5 @@ def main():
13
13
 
14
14
  if __name__ == "__main__":
15
15
  # typer.run(hello)
16
+ main()
16
17
  pass
@@ -1,6 +1,8 @@
1
1
  from pathlib import Path
2
2
  from typing import Optional, Any
3
3
 
4
+ from datetime import datetime, timezone, timedelta
5
+
4
6
 
5
7
  def randstr(length: int = 10, lower: bool = True, upper: bool = True, digits: bool = True, punctuation: bool = False, safe: bool = False, noun: bool = False) -> str:
6
8
  if safe:
@@ -18,16 +20,52 @@ def randstr(length: int = 10, lower: bool = True, upper: bool = True, digits: bo
18
20
  return "".join(random.choices(population, k=length))
19
21
 
20
22
 
21
- def split[T](iterable: list[T], every: int = 1, to: Optional[int] = None) -> list[list[T]]:
23
+ def split_timeframe(start_dt: str, end_dt: str, resolution_ms: int, to: Optional[int]=None, every_ms: Optional[int]=None) -> list[tuple[datetime, datetime]]:
24
+ if (to is None) == (every_ms is None):
25
+ raise ValueError("Exactly one of 'to' or 'every_ms' must be provided, not both or neither")
26
+ start_dt_obj = datetime.fromisoformat(start_dt).replace(tzinfo=timezone.utc)
27
+ end_dt_obj = datetime.fromisoformat(end_dt).replace(tzinfo=timezone.utc)
28
+ delta = end_dt_obj - start_dt_obj
29
+ resolution = timedelta(milliseconds=resolution_ms)
30
+ res: list[tuple[datetime, datetime]] = []
31
+ if to is not None:
32
+ split_size_seconds: float = delta.total_seconds() / to
33
+ split_size_rounded: float = round(split_size_seconds / 60) * 60
34
+ split_size = timedelta(seconds=split_size_rounded)
35
+
36
+ for idx in range(to):
37
+ start = start_dt_obj + split_size * idx
38
+ assert start < end_dt_obj
39
+ if idx == to - 1:
40
+ end = end_dt_obj
41
+ else:
42
+ end = start_dt_obj + split_size * (idx + 1) - resolution
43
+ res.append((start, end))
44
+ else:
45
+ if every_ms is None:
46
+ raise ValueError("every_ms cannot be None when to is None")
47
+ split_size = timedelta(milliseconds=every_ms)
48
+ current_start = start_dt_obj
49
+ while current_start < end_dt_obj:
50
+ current_end = min(current_start + split_size - resolution, end_dt_obj)
51
+ res.append((current_start, current_end))
52
+ current_start += split_size
53
+ return res
54
+ def split_list[T](sequence: list[T], every: Optional[int]=None, to: Optional[int]=None) -> list[list[T]]:
55
+ if (every is None) == (to is None):
56
+ raise ValueError("Exactly one of 'every' or 'to' must be provided, not both or neither")
57
+ if len(sequence) == 0:
58
+ return []
22
59
  import math
23
-
24
- every = every if to is None else math.ceil(len(iterable) / to)
60
+ if to is not None:
61
+ every = math.ceil(len(sequence) / to)
62
+ assert every is not None
25
63
  res: list[list[T]] = []
26
- for ix in range(0, len(iterable), every):
27
- if ix + every < len(iterable):
28
- tmp = iterable[ix : ix + every]
64
+ for ix in range(0, len(sequence), every):
65
+ if ix + every < len(sequence):
66
+ tmp = sequence[ix : ix + every]
29
67
  else:
30
- tmp = iterable[ix : len(iterable)]
68
+ tmp = sequence[ix : len(sequence)]
31
69
  res.append(list(tmp))
32
70
  return list(res)
33
71
 
@@ -4,7 +4,7 @@ Type definitions for the standardized layout configuration schema.
4
4
  This module defines the data structures that match the layout.json schema.
5
5
  """
6
6
 
7
- from typing import TypedDict, List
7
+ from typing import TypedDict, List, Literal
8
8
 
9
9
 
10
10
  class TabConfig(TypedDict):
@@ -27,3 +27,35 @@ class LayoutsFile(TypedDict):
27
27
 
28
28
  version: str
29
29
  layouts: List[LayoutConfig]
30
+
31
+
32
+ def serialize_layouts_to_file(layouts: list[LayoutConfig], version: Literal["0.1"], path: str):
33
+ """Serialize a LayoutConfig to a JSON string."""
34
+ import json
35
+ layout_file: LayoutsFile = {
36
+ "version": version,
37
+ "layouts": layouts,
38
+ }
39
+ # add "$schema" key pointing to https://bit.ly/cfglayout
40
+ layout_dict = {
41
+ "$schema": "https://bit.ly/cfglayout",
42
+ **layout_file
43
+ }
44
+ json_string = json.dumps(layout_dict, indent=4)
45
+ from pathlib import Path
46
+ p = Path(path)
47
+ p.parent.mkdir(parents=True, exist_ok=True)
48
+ if not p.exists():
49
+ p.write_text(json_string, encoding="utf-8")
50
+ return None
51
+ existing_content_layout: LayoutsFile = json.loads(p.read_text(encoding="utf-8"))
52
+ # policy: if layout with same name exists, replace it. we don't lool at tabConfig differences.
53
+ for a_new_layout in layouts:
54
+ for i, existing_layout in enumerate(existing_content_layout["layouts"]):
55
+ if existing_layout["layoutName"] == a_new_layout["layoutName"]:
56
+ existing_content_layout["layouts"][i] = a_new_layout
57
+ break
58
+ else:
59
+ existing_content_layout["layouts"].append(a_new_layout)
60
+ p.write_text(json.dumps(existing_content_layout, indent=4), encoding="utf-8")
61
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machineconfig
3
- Version: 3.85
3
+ Version: 3.88
4
4
  Summary: Dotfiles management package
5
5
  Author-email: Alex Al-Saffar <programmer@usa.com>
6
6
  License: Apache 2.0
@@ -172,13 +172,13 @@ machineconfig/scripts/python/devops_backup_retrieve.py,sha256=jZe5Vki7E2GCMG8hvq
172
172
  machineconfig/scripts/python/devops_devapps_install.py,sha256=Q2suPkfwwdtIN3mjxH6tGZLYC7tZVxdxrGW7d9phiPA,9972
173
173
  machineconfig/scripts/python/devops_update_repos.py,sha256=c5qBc9cuTGDEqDHufkjDT4d_vvJsswv3tlqk9MAulYk,8063
174
174
  machineconfig/scripts/python/dotfile.py,sha256=SRcX-9Ak1jRvF-killBTTm2IWcsNxfiLucH6ZsytAFA,2202
175
- machineconfig/scripts/python/fire_agents.py,sha256=_k1CcPaAp3B7h72tSczFDbLsqTg6FmPDgxxU-GjRHWA,9179
175
+ machineconfig/scripts/python/fire_agents.py,sha256=Hn27ZGWBRlu3mDyJPf_qg_m0i3AbWbWpA-Ce1VeZjUY,9207
176
176
  machineconfig/scripts/python/fire_agents_help_launch.py,sha256=sTdjNz2pDinDMMjUAMN7OqH-KAUeHh6Aihr_zUvtM6k,6128
177
177
  machineconfig/scripts/python/fire_agents_help_search.py,sha256=qIfSS_su2YJ1Gb0_lu4cbjlJlYMBw0v52NTGiSrGjk8,2991
178
178
  machineconfig/scripts/python/fire_agents_load_balancer.py,sha256=QPiCbQq9j5REHStPdYqQcGNkz_rp5CjotqOpMY3v5TM,2099
179
179
  machineconfig/scripts/python/fire_jobs.py,sha256=qD_9JtoNTjOjAyX0_QPysfHNQ-qq55t-Tv9ctEK17MI,20569
180
180
  machineconfig/scripts/python/fire_jobs_args_helper.py,sha256=VsyPgjWRByZgXz65vmmpyR-2mJo6KwNgwrWFYd3EYqc,2075
181
- machineconfig/scripts/python/fire_jobs_layout_helper.py,sha256=Hj77uKgmNKSEBtnW0oCdRBwdKEuhzPxX1p81mRTBibo,3314
181
+ machineconfig/scripts/python/fire_jobs_layout_helper.py,sha256=LHLyp5HxKwut19x2OYKQq8gy4EKon6wNb-jlxx5DoVM,4134
182
182
  machineconfig/scripts/python/fire_jobs_streamlit_helper.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
183
183
  machineconfig/scripts/python/ftpx.py,sha256=vqwhfkg3jVaAdjlWvrcPms2pUD7_c370jn2W6o5XArM,10269
184
184
  machineconfig/scripts/python/get_zellij_cmd.py,sha256=e35-18hoXM9N3PFbvbizfkNY_-63iMicieWE3TbGcCQ,576
@@ -188,7 +188,8 @@ machineconfig/scripts/python/mount_nw_drive.py,sha256=iru6AtnTyvyuk6WxlK5R4lDkul
188
188
  machineconfig/scripts/python/mount_ssh.py,sha256=rGY2pgtlnWMi0Rrge1aCdjtfbULrj2cyaStDoX-y2w4,2236
189
189
  machineconfig/scripts/python/onetimeshare.py,sha256=bmGsNnskym5OWfIhpOfZG5jq3m89FS0a6dF5Sb8LaZM,2539
190
190
  machineconfig/scripts/python/pomodoro.py,sha256=SPkfeoZGv8rylGiOyzQ7UK3aXZ3G2FIOuGkSuBUggOI,2019
191
- machineconfig/scripts/python/repos.py,sha256=gU1gmYmiGTGDlZyIj3b1bC78yV5XZSXEkDD95WRqUnM,7376
191
+ machineconfig/scripts/python/repos.py,sha256=vFlRA9BHXJau5BcIvQXShML3G6Xgv6aVuMqECo9PtfE,4258
192
+ machineconfig/scripts/python/repos_helper_action.py,sha256=f0vFjPj9WEA361961ux3SIEg9riVGHtyuf6BnO6lnvU,13336
192
193
  machineconfig/scripts/python/repos_helper_clone.py,sha256=xW5YZEoNt3k7h9NIULhUhOnh53-B63eiXF2FjOl1IKQ,5535
193
194
  machineconfig/scripts/python/repos_helper_record.py,sha256=YEEQORfEiLddOIIgePo5eEkyQUFruFg3kc8npMvRL-o,10927
194
195
  machineconfig/scripts/python/repos_helper_update.py,sha256=AYyKIB7eQ48yoYmFjydIhRI1lV39TBv_S4_LCa-oKuQ,11042
@@ -197,7 +198,7 @@ machineconfig/scripts/python/share_terminal.py,sha256=divO0lleaX1l90ta4muv_ogUB7
197
198
  machineconfig/scripts/python/snapshot.py,sha256=aDvKeoniZaeTSNv9zWBUajaj2yagAxVdfuvO1_tgq5Y,1026
198
199
  machineconfig/scripts/python/start_slidev.py,sha256=U5ujAL7R5Gd5CzFReTsnF2SThjY91aFBg0Qz_MMl6U4,4573
199
200
  machineconfig/scripts/python/start_terminals.py,sha256=DRWbMZumhPmL0DvvsCsbRNFL5AVQn1SgaziafTio3YQ,6149
200
- machineconfig/scripts/python/t4.py,sha256=7zxjqDjsv8T9tCPUj6_3qOMc80YO7GyXzUM5IP_7iqQ,222
201
+ machineconfig/scripts/python/t4.py,sha256=cZ45iWzS254V5pmVvq8wCw7jAuDhzr8G2Arzay76saU,316
201
202
  machineconfig/scripts/python/viewer.py,sha256=heQNjB9fwn3xxbPgMofhv1Lp6Vtkl76YjjexWWBM0pM,2041
202
203
  machineconfig/scripts/python/viewer_template.py,sha256=ve3Q1-iKhCLc0VJijKvAeOYp2xaFOeIOC_XW956GWCc,3944
203
204
  machineconfig/scripts/python/wifi_conn.py,sha256=4GdLhgma9GRmZ6OFg3oxOX-qY3sr45njPckozlpM_A0,15566
@@ -393,7 +394,7 @@ machineconfig/setup_windows/wt_and_pwsh/install_fonts.ps1,sha256=JsQfGAMkvirhiUm
393
394
  machineconfig/setup_windows/wt_and_pwsh/install_nerd_fonts.py,sha256=o9K7QMIw1cBdMxLQsOk64MAy1YgNNhPRYura8cdTLXI,4707
394
395
  machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py,sha256=rZZJamy3YxAeJhdMIFR6IWtjgn1u1HUdbk1J24NtryE,6116
395
396
  machineconfig/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
396
- machineconfig/utils/accessories.py,sha256=6vw2C8QEroNRjHDcLC7clKlIWjUX5cJXb_65KN8wZTg,2484
397
+ machineconfig/utils/accessories.py,sha256=W_9dLzjwNTW5JQk_pe3B2ijQ1nA2-8Kdg2r7VBtzgQs,4340
397
398
  machineconfig/utils/code.py,sha256=pKPHInKgXJWeACVbxuE7sMdYeZCbNttaYCsfonGhFfc,4464
398
399
  machineconfig/utils/installer.py,sha256=_sjaIqqHN-7n3mYD-iOIli6Bn2792EjOwE-JC528Qfo,11936
399
400
  machineconfig/utils/io.py,sha256=6LuQMT7CG26atx5_0P30Ru0zHgLwuvpKHfZLUWIjS-U,2873
@@ -419,10 +420,10 @@ machineconfig/utils/installer_utils/installer_abc.py,sha256=iB1_PZLQGouCdEA8bixd
419
420
  machineconfig/utils/installer_utils/installer_class.py,sha256=P3ZKr92b0ofbQCJUyK3eU3FVgK1ZMRw36WH-rbR5pAw,20430
420
421
  machineconfig/utils/schemas/fire_agents/fire_agents_input.py,sha256=CCs5ebomW1acKWZRpv9dyDzM-W6pwvVplikcutE2D8I,2339
421
422
  machineconfig/utils/schemas/installer/installer_types.py,sha256=iAzcALc9z_FAQE9iuGHfX6Z0B1_n3Gt6eC0d6heYik0,599
422
- machineconfig/utils/schemas/layouts/layout_types.py,sha256=OmiOX9xtakPz4l6IobWnpFHpbn95fitEE9q0YL1WxjQ,617
423
+ machineconfig/utils/schemas/layouts/layout_types.py,sha256=M1ZFCz_kjRZPhxM19rIYUDR5lDDpwa09odR_ihtIFq0,1932
423
424
  machineconfig/utils/schemas/repos/repos_types.py,sha256=ECVr-3IVIo8yjmYmVXX2mnDDN1SLSwvQIhx4KDDQHBQ,405
424
- machineconfig-3.85.dist-info/METADATA,sha256=3OnrbCTgB0CwtK85fP2bkhm8aIF0iGIS46ZXUYu87xk,6998
425
- machineconfig-3.85.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
426
- machineconfig-3.85.dist-info/entry_points.txt,sha256=rSx_9gXd2stziS1OkNy__jF647hrRxRiF6zolLUELc4,1153
427
- machineconfig-3.85.dist-info/top_level.txt,sha256=porRtB8qms8fOIUJgK-tO83_FeH6Bpe12oUVC670teA,14
428
- machineconfig-3.85.dist-info/RECORD,,
425
+ machineconfig-3.88.dist-info/METADATA,sha256=9lxn7WjqHpXrQZf633ZzqR190I1ZPVSqK0arz77rq_E,6998
426
+ machineconfig-3.88.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
427
+ machineconfig-3.88.dist-info/entry_points.txt,sha256=rSx_9gXd2stziS1OkNy__jF647hrRxRiF6zolLUELc4,1153
428
+ machineconfig-3.88.dist-info/top_level.txt,sha256=porRtB8qms8fOIUJgK-tO83_FeH6Bpe12oUVC670teA,14
429
+ machineconfig-3.88.dist-info/RECORD,,