machineconfig 7.64__py3-none-any.whl → 7.83__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.

Files changed (104) hide show
  1. machineconfig/cluster/sessions_managers/utils/maker.py +4 -2
  2. machineconfig/jobs/installer/custom/yazi.py +120 -0
  3. machineconfig/jobs/installer/custom_dev/nerdfont.py +1 -1
  4. machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +26 -12
  5. machineconfig/jobs/installer/custom_dev/sysabc.py +26 -5
  6. machineconfig/jobs/installer/installer_data.json +232 -96
  7. machineconfig/jobs/installer/powershell_scripts/install_fonts.ps1 +129 -34
  8. machineconfig/profile/create_helper.py +0 -12
  9. machineconfig/profile/create_links_export.py +2 -2
  10. machineconfig/profile/mapper.toml +2 -2
  11. machineconfig/scripts/__init__.py +0 -4
  12. machineconfig/scripts/python/agents.py +22 -17
  13. machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +4 -0
  14. machineconfig/scripts/python/croshell.py +22 -17
  15. machineconfig/scripts/python/devops.py +1 -1
  16. machineconfig/scripts/python/devops_navigator.py +0 -4
  17. machineconfig/scripts/python/env_manager/env_manager_tui.py +204 -0
  18. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  19. machineconfig/scripts/python/fire_jobs.py +13 -13
  20. machineconfig/scripts/python/ftpx.py +36 -12
  21. machineconfig/scripts/python/helpers/ast_search.py +74 -0
  22. machineconfig/scripts/python/helpers/qr_code.py +166 -0
  23. machineconfig/scripts/python/helpers/repo_rag.py +325 -0
  24. machineconfig/scripts/python/helpers/symantic_search.py +25 -0
  25. machineconfig/scripts/python/helpers_cloud/cloud_copy.py +28 -21
  26. machineconfig/scripts/python/helpers_cloud/cloud_helpers.py +1 -1
  27. machineconfig/scripts/python/helpers_cloud/cloud_mount.py +19 -17
  28. machineconfig/scripts/python/helpers_cloud/cloud_sync.py +8 -7
  29. machineconfig/scripts/python/helpers_croshell/start_slidev.py +6 -7
  30. machineconfig/scripts/python/helpers_devops/cli_config.py +10 -0
  31. machineconfig/scripts/python/helpers_devops/cli_nw.py +90 -10
  32. machineconfig/scripts/python/helpers_devops/cli_self.py +8 -7
  33. machineconfig/scripts/python/helpers_devops/cli_share_file.py +7 -7
  34. machineconfig/scripts/python/helpers_devops/cli_share_server.py +12 -11
  35. machineconfig/scripts/python/helpers_devops/cli_terminal.py +8 -10
  36. machineconfig/scripts/python/helpers_devops/cli_utils.py +2 -1
  37. machineconfig/scripts/python/helpers_devops/devops_status.py +7 -19
  38. machineconfig/scripts/python/helpers_fire_command/fire_jobs_route_helper.py +20 -9
  39. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfg +2 -2
  40. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfg.ps1 +58 -1
  41. machineconfig/scripts/python/helpers_navigator/command_tree.py +50 -18
  42. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +5 -3
  43. machineconfig/scripts/python/helpers_repos/count_lines.py +40 -11
  44. machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
  45. machineconfig/scripts/python/helpers_utils/download.py +4 -3
  46. machineconfig/scripts/python/helpers_utils/path.py +87 -34
  47. machineconfig/scripts/python/interactive.py +1 -1
  48. machineconfig/scripts/python/{machineconfig.py → mcfg_entry.py} +4 -0
  49. machineconfig/scripts/python/msearch.py +55 -6
  50. machineconfig/scripts/python/nw/address.py +132 -0
  51. machineconfig/scripts/python/nw/devops_add_ssh_key.py +8 -5
  52. machineconfig/scripts/python/terminal.py +2 -2
  53. machineconfig/scripts/python/utils.py +12 -11
  54. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  55. machineconfig/settings/lf/windows/lfcd.ps1 +1 -1
  56. machineconfig/settings/shells/nushell/config.nu +2 -2
  57. machineconfig/settings/shells/nushell/env.nu +45 -6
  58. machineconfig/settings/shells/nushell/init.nu +282 -95
  59. machineconfig/settings/shells/pwsh/init.ps1 +1 -0
  60. machineconfig/settings/yazi/init.lua +4 -0
  61. machineconfig/settings/yazi/keymap_linux.toml +11 -4
  62. machineconfig/settings/yazi/theme.toml +4 -0
  63. machineconfig/settings/yazi/yazi_linux.toml +84 -0
  64. machineconfig/settings/yazi/yazi_windows.toml +58 -0
  65. machineconfig/setup_linux/web_shortcuts/interactive.sh +10 -10
  66. machineconfig/setup_windows/uv.ps1 +8 -1
  67. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +10 -10
  68. machineconfig/setup_windows/web_shortcuts/quick_init.ps1 +3 -2
  69. machineconfig/utils/accessories.py +7 -4
  70. machineconfig/utils/code.py +4 -2
  71. machineconfig/utils/installer_utils/github_release_bulk.py +104 -62
  72. machineconfig/utils/installer_utils/install_from_url.py +200 -0
  73. machineconfig/utils/installer_utils/installer_class.py +25 -74
  74. machineconfig/utils/installer_utils/installer_cli.py +40 -50
  75. machineconfig/utils/installer_utils/installer_helper.py +100 -0
  76. machineconfig/utils/installer_utils/installer_runner.py +5 -8
  77. machineconfig/utils/links.py +2 -2
  78. machineconfig/utils/meta.py +2 -2
  79. machineconfig/utils/options.py +3 -3
  80. machineconfig/utils/path_extended.py +1 -1
  81. machineconfig/utils/path_helper.py +0 -1
  82. machineconfig/utils/ssh.py +143 -409
  83. machineconfig/utils/ssh_utils/abc.py +8 -0
  84. machineconfig/utils/ssh_utils/copy_from_here.py +110 -0
  85. machineconfig/utils/ssh_utils/copy_to_here.py +302 -0
  86. machineconfig/utils/ssh_utils/utils.py +141 -0
  87. machineconfig/utils/ssh_utils/wsl.py +210 -0
  88. machineconfig/utils/upgrade_packages.py +2 -1
  89. machineconfig/utils/ve.py +11 -4
  90. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/METADATA +2 -2
  91. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/RECORD +96 -89
  92. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/entry_points.txt +2 -2
  93. machineconfig/scripts/python/explore.py +0 -49
  94. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfag +0 -17
  95. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfrga +0 -21
  96. machineconfig/scripts/python/helpers_msearch/scripts_linux/skrg +0 -4
  97. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfb.ps1 +0 -3
  98. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfrga.bat +0 -20
  99. machineconfig/settings/yazi/yazi.toml +0 -17
  100. machineconfig/setup_linux/others/cli_installation.sh +0 -137
  101. /machineconfig/{settings/shells/pwsh/profile.ps1 → scripts/python/helpers_fire_command/f.py} +0 -0
  102. /machineconfig/scripts/{Restore-ThunderbirdProfile.ps1 → windows/mounts/Restore-ThunderbirdProfile.ps1} +0 -0
  103. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/WHEEL +0 -0
  104. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.13"
4
+ # dependencies = [
5
+ # "machineconfig>=7.83",
6
+ # "textual",
7
+ # "pyperclip",
8
+ # ]
9
+ # ///
10
+
11
+
12
+
13
+ import os
14
+ import platform
15
+ from collections.abc import Mapping
16
+ from typing import Final
17
+
18
+ from rich.text import Text
19
+ from textual import on
20
+ from textual.app import App, ComposeResult
21
+ from textual.binding import Binding
22
+ from textual.containers import Horizontal, Vertical
23
+ from textual.widgets import Footer, Header, Label, ListItem, ListView, Static
24
+
25
+
26
+ VALUE_PREVIEW_LIMIT: Final[int] = 4096
27
+ SUMMARY_LIMIT: Final[int] = 96
28
+
29
+
30
+ def truncate_text(text: str, limit: int) -> tuple[str, int]:
31
+ length = len(text)
32
+ if length <= limit:
33
+ return text, 0
34
+ return text[:limit], length - limit
35
+
36
+
37
+ def format_summary(env_key: str, env_value: str, limit: int) -> str:
38
+ sanitized = env_value.replace("\n", "\\n").replace("\t", "\\t")
39
+ preview, remainder = truncate_text(sanitized, limit)
40
+ if preview == "":
41
+ base = f"{env_key} = <empty>"
42
+ else:
43
+ base = f"{env_key} = {preview}"
44
+ if remainder == 0:
45
+ return base
46
+ return f"{base}... (+{remainder} chars)"
47
+
48
+
49
+ def collect_environment(env: Mapping[str, str]) -> list[tuple[str, str]]:
50
+ return sorted(env.items(), key=lambda pair: pair[0].lower())
51
+
52
+
53
+ class EnvListItem(ListItem):
54
+ def __init__(self, env_key: str, summary: str) -> None:
55
+ super().__init__(Label(summary))
56
+ self._env_key = env_key
57
+
58
+ def env_key(self) -> str:
59
+ return self._env_key
60
+
61
+
62
+ class EnvValuePreview(Static):
63
+ def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
64
+ super().__init__(*args, **kwargs)
65
+ self.border_title = "Environment Value"
66
+
67
+ def show_value(self, env_key: str, env_value: str) -> None:
68
+ preview, remainder = truncate_text(env_value, VALUE_PREVIEW_LIMIT)
69
+ text = Text()
70
+ text.append(f"{env_key}\n\n", style="bold cyan")
71
+ if preview == "":
72
+ text.append("<empty>", style="dim")
73
+ else:
74
+ text.append(preview)
75
+ if remainder > 0:
76
+ text.append(f"\n... truncated {remainder} characters", style="yellow")
77
+ self.update(text)
78
+
79
+
80
+ class StatusBar(Static):
81
+ def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
82
+ super().__init__(*args, **kwargs)
83
+ self.border_title = "Status"
84
+
85
+ def show_message(self, message: str, level: str) -> None:
86
+ palette = {
87
+ "info": "cyan",
88
+ "success": "green",
89
+ "warning": "yellow",
90
+ "error": "red",
91
+ }
92
+ color = palette.get(level, "white")
93
+ self.update(f"[{color}]{message}[/{color}]")
94
+
95
+
96
+ class EnvExplorerApp(App[None]):
97
+ CSS = """
98
+ Screen { background: $surface; }
99
+ Header { background: $primary; color: $text; }
100
+ Footer { background: $panel; }
101
+ #main-container { height: 100%; }
102
+ #left-panel { width: 50%; height: 100%; border: solid $primary; padding: 1; }
103
+ #right-panel { width: 50%; height: 100%; border: solid $accent; padding: 1; }
104
+ ListView { height: 1fr; border: solid $accent; background: $surface; }
105
+ ListView > ListItem { padding: 0 1; }
106
+ EnvValuePreview { height: 1fr; border: solid $primary; background: $surface; padding: 1; overflow-y: auto; }
107
+ StatusBar { height: 3; border: solid $success; background: $surface; padding: 1; }
108
+ Label { padding: 0 1; height: auto; }
109
+ """
110
+
111
+ BINDINGS = [
112
+ Binding("q", "quit", "Quit", show=True),
113
+ Binding("r", "refresh", "Refresh", show=True),
114
+ Binding("c", "copy_entry", "Copy", show=True),
115
+ ]
116
+
117
+ def __init__(self) -> None:
118
+ super().__init__()
119
+ self._env_pairs: list[tuple[str, str]] = []
120
+ self._env_lookup: dict[str, str] = {}
121
+ self._selected_key: str = ""
122
+
123
+ def compose(self) -> ComposeResult:
124
+ platform_name = platform.system()
125
+ yield Header(show_clock=True)
126
+ with Horizontal(id="main-container"):
127
+ with Vertical(id="left-panel"):
128
+ yield Label(f"🌐 Environment Variables ({platform_name})")
129
+ yield ListView(id="env-list")
130
+ with Vertical(id="right-panel"):
131
+ yield EnvValuePreview(id="preview")
132
+ yield StatusBar(id="status")
133
+ yield Footer()
134
+
135
+ def on_mount(self) -> None:
136
+ self.title = "Environment Explorer"
137
+ self.sub_title = f"Platform: {platform.system()}"
138
+ self._reload_environment()
139
+ self._status().show_message("Ready. Select a variable to preview its value.", "info")
140
+
141
+ def _reload_environment(self) -> None:
142
+ self._env_pairs = collect_environment(os.environ)
143
+ self._env_lookup = dict(self._env_pairs)
144
+ self._populate_list()
145
+
146
+ def _populate_list(self) -> None:
147
+ list_view = self.query_one("#env-list", ListView)
148
+ list_view.clear()
149
+ for env_key, env_value in self._env_pairs:
150
+ summary = format_summary(env_key, env_value, SUMMARY_LIMIT)
151
+ list_view.append(EnvListItem(env_key, summary))
152
+ self._status().show_message(f"Loaded {len(self._env_pairs)} environment variables.", "success")
153
+
154
+ def _status(self) -> StatusBar:
155
+ return self.query_one("#status", StatusBar)
156
+
157
+ def _preview(self) -> EnvValuePreview:
158
+ return self.query_one("#preview", EnvValuePreview)
159
+
160
+ @on(ListView.Highlighted)
161
+ def handle_highlight(self, event: ListView.Highlighted) -> None:
162
+ if not isinstance(event.item, EnvListItem):
163
+ return
164
+ env_key = event.item.env_key()
165
+ env_value = self._env_lookup.get(env_key, "")
166
+ self._preview().show_value(env_key, env_value)
167
+ self._status().show_message(f"Previewing {env_key}", "info")
168
+
169
+ @on(ListView.Selected)
170
+ def handle_selection(self, event: ListView.Selected) -> None:
171
+ if not isinstance(event.item, EnvListItem):
172
+ return
173
+ env_key = event.item.env_key()
174
+ self._selected_key = env_key
175
+ env_value = self._env_lookup.get(env_key, "")
176
+ self._preview().show_value(env_key, env_value)
177
+ self._status().show_message(f"Selected {env_key}", "success")
178
+
179
+ def action_refresh(self) -> None:
180
+ self._reload_environment()
181
+ self._status().show_message("Environment reloaded.", "success")
182
+
183
+ def action_copy_entry(self) -> None:
184
+ if self._selected_key == "":
185
+ self._status().show_message("No variable selected.", "warning")
186
+ return
187
+ env_value = self._env_lookup.get(self._selected_key, "")
188
+ payload = f"{self._selected_key}={env_value}"
189
+ try:
190
+ import pyperclip # type: ignore[import]
191
+
192
+ pyperclip.copy(payload)
193
+ self._status().show_message(f"Copied {self._selected_key} to clipboard.", "success")
194
+ except ImportError:
195
+ self._status().show_message("pyperclip unavailable. Install it for clipboard support.", "warning")
196
+
197
+
198
+ def main() -> None:
199
+ app = EnvExplorerApp()
200
+ app.run()
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()
@@ -2,7 +2,7 @@
2
2
  # /// script
3
3
  # requires-python = ">=3.13"
4
4
  # dependencies = [
5
- # "machineconfig>=7.64",
5
+ # "machineconfig>=7.83",
6
6
  # "textual",
7
7
  # "pyperclip",
8
8
  # ]
@@ -7,20 +7,16 @@ fire
7
7
 
8
8
  """
9
9
 
10
- from machineconfig.utils.ve import get_ve_path_and_ipython_profile
11
- from machineconfig.utils.accessories import get_repo_root, randstr
12
- from machineconfig.scripts.python.helpers_fire_command.fire_jobs_args_helper import FireJobArgs, extract_kwargs, parse_fire_args_from_context
13
- from machineconfig.utils.path_helper import get_choice_file
14
-
15
- import platform
16
10
  from typing import Optional, Annotated
17
- from pathlib import Path
18
11
  import typer
19
12
 
20
13
 
21
- def route(args: FireJobArgs, fire_args: str = "") -> None:
14
+ def route(args: "FireJobArgs", fire_args: str = "") -> None:
15
+ from pathlib import Path
16
+ from machineconfig.utils.path_helper import get_choice_file
17
+ from machineconfig.utils.accessories import get_repo_root, randstr
22
18
  choice_file = get_choice_file(args.path, suffixes=None)
23
- repo_root = get_repo_root(Path(choice_file))
19
+ repo_root = get_repo_root(choice_file)
24
20
  print(f"💾 Selected file: {choice_file}.\nRepo root: {repo_root}")
25
21
  if args.marimo:
26
22
  print(f"🧽 Preparing to launch Marimo notebook for `{choice_file}`...")
@@ -38,6 +34,7 @@ uv run --project {repo_root} --with marimo marimo edit --host 0.0.0.0 marimo_nb.
38
34
 
39
35
  # ========================= preparing kwargs_dict
40
36
  if choice_file.suffix == ".py":
37
+ from machineconfig.scripts.python.helpers_fire_command.fire_jobs_args_helper import extract_kwargs
41
38
  kwargs_dict = extract_kwargs(args) # This now returns empty dict, but kept for compatibility
42
39
  else:
43
40
  kwargs_dict = {}
@@ -52,17 +49,17 @@ uv run --project {repo_root} --with marimo marimo edit --host 0.0.0.0 marimo_nb.
52
49
  choice_function = args.function
53
50
 
54
51
  if choice_file.suffix == ".py":
55
- from machineconfig.scripts.python.helpers_fire_command.fire_jobs_route_helper import get_command_streamlit
56
-
57
52
  with_project = f"--project {repo_root} " if repo_root is not None else ""
58
53
  if args.streamlit:
54
+ from machineconfig.scripts.python.helpers_fire_command.fire_jobs_route_helper import get_command_streamlit
59
55
  exe = get_command_streamlit(choice_file=choice_file, environment=args.environment, repo_root=repo_root)
60
56
  exe = f"uv run {with_project} {exe} "
61
57
  elif args.jupyter:
62
58
  exe = f"uv run {with_project} jupyter-lab"
63
59
  else:
64
60
  if args.interactive:
65
- _ve_root_from_file, ipy_profile = get_ve_path_and_ipython_profile(choice_file)
61
+ from machineconfig.utils.ve import get_ve_path_and_ipython_profile
62
+ _ve_root_from_file, ipy_profile = get_ve_path_and_ipython_profile(init_path=choice_file)
66
63
  if ipy_profile is None:
67
64
  ipy_profile = "default"
68
65
  exe = f"uv run {with_project} ipython -i --no-banner --profile {ipy_profile} "
@@ -106,6 +103,7 @@ uv run --project {repo_root} --with marimo marimo edit --host 0.0.0.0 marimo_nb.
106
103
 
107
104
  # ========================= determining basic command structure: putting together exe & choice_file & choice_function & pdb
108
105
  if args.debug:
106
+ import platform
109
107
  if platform.system() == "Windows":
110
108
  command = f"{exe} -m ipdb {choice_file} " # pudb is not available on windows machines, use poor man's debugger instead.
111
109
  elif platform.system() in ["Linux", "Darwin"]:
@@ -173,6 +171,7 @@ uv run --project {repo_root} --with marimo marimo edit --host 0.0.0.0 marimo_nb.
173
171
  export_line = add_to_path(path_variable="PYTHONPATH", directory=str(repo_root))
174
172
  command = export_line + "\n" + command
175
173
  if args.loop:
174
+ import platform
176
175
  if platform.system() in ["Linux", "Darwin"]:
177
176
  command = command + "\nsleep 0.5"
178
177
  elif platform.system() == "Windows":
@@ -214,6 +213,7 @@ def fire(
214
213
  """Main function to process fire jobs arguments."""
215
214
 
216
215
  # Get Fire arguments from context
216
+ from machineconfig.scripts.python.helpers_fire_command.fire_jobs_args_helper import FireJobArgs, parse_fire_args_from_context
217
217
  fire_args = parse_fire_args_from_context(ctx)
218
218
 
219
219
  args = FireJobArgs(
@@ -266,4 +266,4 @@ def main():
266
266
 
267
267
 
268
268
  if __name__ == "__main__":
269
- pass
269
+ from machineconfig.scripts.python.helpers_fire_command.fire_jobs_args_helper import FireJobArgs
@@ -6,17 +6,7 @@ Currently, the only way to work around this is to predifine the host in ~/.ssh/c
6
6
  """
7
7
 
8
8
  import typer
9
- from typing_extensions import Annotated
10
- from rich.console import Console
11
- from rich.panel import Panel
12
-
13
- from machineconfig.utils.ssh import SSH
14
- from machineconfig.utils.path_extended import PathExtended
15
- from machineconfig.scripts.python.helpers_cloud.helpers2 import ES
16
- from machineconfig.utils.accessories import pprint
17
-
18
-
19
- console = Console()
9
+ from typing import Annotated
20
10
 
21
11
 
22
12
  def ftpx(
@@ -25,7 +15,41 @@ def ftpx(
25
15
  recursive: Annotated[bool, typer.Option("--recursive", "-r", help="Send recursively.")] = False,
26
16
  zipFirst: Annotated[bool, typer.Option("--zipFirst", "-z", help="Zip before sending.")] = False,
27
17
  cloud: Annotated[bool, typer.Option("--cloud", "-c", help="Transfer through the cloud.")] = False,
18
+ overwrite_existing: Annotated[bool, typer.Option("--overwrite-existing", "-o", help="Overwrite existing files on remote when sending from local to remote.")] = False,
28
19
  ) -> None:
20
+ from pathlib import Path
21
+ if target == "wsl" or source == "wsl":
22
+ from machineconfig.utils.ssh_utils.wsl import copy_when_inside_windows
23
+ if target == "wsl":
24
+ target_obj = Path(source).expanduser().absolute().relative_to(Path.home())
25
+ source_obj = target_obj
26
+ else:
27
+ source_obj = Path(target).expanduser().absolute().relative_to(Path.home())
28
+ target_obj = source_obj
29
+ copy_when_inside_windows(source_obj, target_obj, overwrite_existing)
30
+ return
31
+ elif source == "win" or target == "win":
32
+ if source == "win":
33
+ source_obj = Path(target).expanduser().absolute().relative_to(Path.home())
34
+ target_obj = source_obj
35
+ else:
36
+ target_obj = Path(source).expanduser().absolute().relative_to(Path.home())
37
+ source_obj = target_obj
38
+ from machineconfig.utils.ssh_utils.wsl import copy_when_inside_wsl
39
+ copy_when_inside_wsl(source_obj, target_obj, overwrite_existing)
40
+ return
41
+
42
+ from rich.console import Console
43
+ from rich.panel import Panel
44
+
45
+ from machineconfig.utils.ssh import SSH
46
+ from machineconfig.utils.path_extended import PathExtended
47
+ from machineconfig.scripts.python.helpers_cloud.helpers2 import ES
48
+ from machineconfig.utils.accessories import pprint
49
+
50
+
51
+ console = Console()
52
+
29
53
  console.print(
30
54
  Panel(
31
55
  "\n".join(
@@ -185,7 +209,7 @@ def ftpx(
185
209
  padding=(1, 2),
186
210
  )
187
211
  )
188
- received_file = ssh.copy_from_here(source_path=resolved_source, target_rel2home=resolved_target, compress_with_zip=zipFirst, recursive=recursive, overwrite_existing=False)
212
+ received_file = ssh.copy_from_here(source_path=resolved_source, target_rel2home=resolved_target, compress_with_zip=zipFirst, recursive=recursive, overwrite_existing=overwrite_existing)
189
213
 
190
214
  if source_is_remote and isinstance(received_file, PathExtended):
191
215
  console.print(
@@ -0,0 +1,74 @@
1
+
2
+ import ast
3
+ import os
4
+ from typing import TypedDict
5
+
6
+
7
+ class SymbolInfo(TypedDict):
8
+ """Represents a symbol (module, class, or function) in the repository."""
9
+ type: str
10
+ name: str
11
+ path: str
12
+ # line: int | None
13
+ # column: int | None
14
+ docstring: str
15
+
16
+
17
+ def _get_docstring(node: ast.AsyncFunctionDef | ast.FunctionDef | ast.ClassDef | ast.Module) -> str:
18
+ """Extract docstring from an AST node."""
19
+ return ast.get_docstring(node) or ""
20
+
21
+
22
+ def _extract_symbols(tree: ast.AST, module_path: str, source: str) -> list[SymbolInfo]:
23
+ """Extract symbols from an AST tree."""
24
+ symbols: list[SymbolInfo] = []
25
+
26
+ for node in ast.walk(tree):
27
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
28
+ symbol: SymbolInfo = {
29
+ "type": "function",
30
+ "name": node.name,
31
+ "path": f"{module_path}.{node.name}",
32
+ "docstring": _get_docstring(node),
33
+ }
34
+ symbols.append(symbol)
35
+ elif isinstance(node, ast.ClassDef):
36
+ symbol: SymbolInfo = {
37
+ "type": "class",
38
+ "name": node.name,
39
+ "path": f"{module_path}.{node.name}",
40
+ "docstring": _get_docstring(node),
41
+ }
42
+ symbols.append(symbol)
43
+
44
+ return symbols
45
+
46
+
47
+ def get_repo_symbols(repo_path: str) -> list[SymbolInfo]:
48
+ skip_dirs = {'.venv', 'venv', '__pycache__', '.mypy_cache', '.pytest_cache', '.git'}
49
+ results: list[SymbolInfo] = []
50
+ counter: int = 0
51
+ for root, dirs, files in os.walk(repo_path):
52
+ dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith('.')]
53
+ for file in files:
54
+ if not file.endswith(".py"):
55
+ continue
56
+ file_path = os.path.join(root, file)
57
+ module_path = (
58
+ os.path.relpath(file_path, repo_path)
59
+ .replace(os.sep, ".")
60
+ .removesuffix(".py")
61
+ )
62
+ try:
63
+ if counter % 100 == 0: print(f"🔍 Parsing {counter}: {file_path}...")
64
+ with open(file_path, encoding="utf-8") as f:
65
+ source = f.read()
66
+ tree = ast.parse(source, filename=file_path)
67
+ symbols = _extract_symbols(tree, module_path, source)
68
+ results.extend(symbols)
69
+ except Exception as e:
70
+ print(f"⚠️ Error parsing {file_path}: {e}")
71
+ continue
72
+ counter += 1
73
+
74
+ return results
@@ -0,0 +1,166 @@
1
+
2
+
3
+ from typing import Literal
4
+
5
+
6
+ def generate_qrcode_grid(
7
+ strings: list[str],
8
+ output_path: str,
9
+ per_row: int = 3,
10
+ qr_size: int = 200,
11
+ label_height: int = 30,
12
+ padding: int = 20,
13
+ label_max_chars: int = 25,
14
+ format: Literal["svg", "png"] = "svg",
15
+ ) -> str:
16
+ from pathlib import Path
17
+ if not strings:
18
+ raise ValueError("strings list cannot be empty")
19
+
20
+ output_path_obj = Path(output_path)
21
+ output_path_obj.parent.mkdir(parents=True, exist_ok=True)
22
+
23
+ if format == "svg":
24
+ return _generate_svg(strings, output_path, per_row, qr_size, label_height, padding, label_max_chars)
25
+ elif format == "png":
26
+ return _generate_png(strings, output_path, per_row, qr_size, label_height, padding, label_max_chars)
27
+ else:
28
+ raise ValueError(f"Unsupported format: {format}")
29
+
30
+
31
+ def _generate_svg(
32
+ strings: list[str],
33
+ output_path: str,
34
+ per_row: int,
35
+ qr_size: int,
36
+ label_height: int,
37
+ padding: int,
38
+ label_max_chars: int,
39
+ ) -> str:
40
+ num_items = len(strings)
41
+ num_rows = (num_items + per_row - 1) // per_row
42
+
43
+ cell_width = qr_size
44
+ cell_height = qr_size + label_height
45
+ total_width = per_row * cell_width + (per_row + 1) * padding
46
+ total_height = num_rows * cell_height + (num_rows + 1) * padding
47
+
48
+ from xml.etree import ElementTree as ET
49
+
50
+ import qrcode
51
+ svg_root = ET.Element(
52
+ "svg",
53
+ xmlns="http://www.w3.org/2000/svg",
54
+ width=str(total_width),
55
+ height=str(total_height),
56
+ viewBox=f"0 0 {total_width} {total_height}",
57
+ )
58
+
59
+ _bg_rect = ET.SubElement(svg_root, "rect", width=str(total_width), height=str(total_height), fill="white")
60
+
61
+ for idx, text in enumerate(strings):
62
+ row = idx // per_row
63
+ col = idx % per_row
64
+
65
+ x_offset = padding + col * (cell_width + padding)
66
+ y_offset = padding + row * (cell_height + padding)
67
+
68
+ qr = qrcode.QRCode(version=1, error_correction=qrcode.ERROR_CORRECT_L, box_size=10, border=2, image_factory=qrcode.image.svg.SvgPathImage) # type: ignore
69
+ qr.add_data(text)
70
+ qr.make(fit=True)
71
+
72
+ qr_img = qr.make_image()
73
+ qr_svg_string = qr_img.to_string(encoding="unicode")
74
+
75
+ qr_tree = ET.fromstring(qr_svg_string)
76
+
77
+ group = ET.SubElement(svg_root, "g", transform=f"translate({x_offset}, {y_offset})")
78
+
79
+ qr_group = ET.SubElement(group, "g")
80
+ for child in qr_tree:
81
+ qr_group.append(child)
82
+
83
+ label_text = text[:label_max_chars] if len(text) > label_max_chars else text
84
+ text_y = qr_size + label_height // 2
85
+
86
+ text_elem = ET.SubElement(
87
+ group,
88
+ "text",
89
+ x=str(qr_size // 2),
90
+ y=str(text_y),
91
+ fill="black",
92
+ attrib={
93
+ "font-family": "monospace",
94
+ "font-size": "12",
95
+ "text-anchor": "middle",
96
+ "dominant-baseline": "middle",
97
+ },
98
+ )
99
+ text_elem.text = label_text
100
+
101
+ tree = ET.ElementTree(svg_root)
102
+ ET.indent(tree, space=" ")
103
+ tree.write(output_path, encoding="unicode", xml_declaration=True)
104
+
105
+ return output_path
106
+
107
+
108
+ def _generate_png(
109
+ strings: list[str],
110
+ output_path: str,
111
+ per_row: int,
112
+ qr_size: int,
113
+ label_height: int,
114
+ padding: int,
115
+ label_max_chars: int,
116
+ ) -> str:
117
+ num_items = len(strings)
118
+ num_rows = (num_items + per_row - 1) // per_row
119
+
120
+ cell_width = qr_size
121
+ cell_height = qr_size + label_height
122
+ total_width = per_row * cell_width + (per_row + 1) * padding
123
+ total_height = num_rows * cell_height + (num_rows + 1) * padding
124
+
125
+ import qrcode
126
+ import qrcode.image.pil
127
+ from PIL import Image, ImageDraw, ImageFont
128
+ img = Image.new("RGB", (total_width, total_height), color="white")
129
+ draw = ImageDraw.Draw(img)
130
+
131
+ try:
132
+ font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 12)
133
+ except OSError:
134
+ try:
135
+ font = ImageFont.truetype("arial.ttf", 12)
136
+ except OSError:
137
+ font = ImageFont.load_default()
138
+
139
+ for idx, text in enumerate(strings):
140
+ row = idx // per_row
141
+ col = idx % per_row
142
+
143
+ x_offset = padding + col * (cell_width + padding)
144
+ y_offset = padding + row * (cell_height + padding)
145
+
146
+ qr = qrcode.QRCode(version=1, error_correction=qrcode.ERROR_CORRECT_L, box_size=10, border=2, image_factory=qrcode.image.pil.PilImage)
147
+ qr.add_data(text)
148
+ qr.make(fit=True)
149
+
150
+ qr_img = qr.make_image(fill_color="black", back_color="white")
151
+ qr_img_resized = qr_img.resize((qr_size, qr_size), Image.Resampling.LANCZOS)
152
+
153
+ img.paste(qr_img_resized, (x_offset, y_offset))
154
+
155
+ label_text = text[:label_max_chars] if len(text) > label_max_chars else text
156
+
157
+ bbox = draw.textbbox((0, 0), label_text, font=font)
158
+ text_width = bbox[2] - bbox[0]
159
+ text_x = x_offset + (qr_size - text_width) // 2
160
+ text_y = y_offset + qr_size + label_height // 2 - 6
161
+
162
+ draw.text((text_x, text_y), label_text, fill="black", font=font)
163
+
164
+ img.save(output_path, format="PNG")
165
+
166
+ return output_path