machineconfig 6.47__py3-none-any.whl → 6.49__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 (73) hide show
  1. machineconfig/cluster/sessions_managers/utils/maker.py +1 -1
  2. machineconfig/scripts/python/cloud.py +3 -3
  3. machineconfig/scripts/python/croshell.py +6 -6
  4. machineconfig/scripts/python/devops.py +6 -6
  5. machineconfig/scripts/python/devops_navigator.py +1 -1
  6. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  7. machineconfig/scripts/python/ftpx.py +1 -1
  8. machineconfig/scripts/python/{cloud_helpers → helpers_cloud}/cloud_copy.py +2 -2
  9. machineconfig/scripts/python/{cloud_helpers → helpers_cloud}/cloud_sync.py +3 -3
  10. machineconfig/scripts/python/{cloud_helpers → helpers_cloud}/helpers2.py +1 -1
  11. machineconfig/scripts/python/{devops_helpers → helpers_devops}/cli_config.py +3 -3
  12. machineconfig/scripts/python/{devops_helpers → helpers_devops}/cli_data.py +2 -2
  13. machineconfig/scripts/python/{devops_helpers → helpers_devops}/cli_nw.py +2 -2
  14. machineconfig/scripts/python/{devops_helpers → helpers_devops}/cli_repos.py +10 -10
  15. machineconfig/scripts/python/{devops_helpers → helpers_devops}/cli_self.py +4 -4
  16. machineconfig/scripts/python/{devops_helpers → helpers_devops}/devops_backup_retrieve.py +1 -1
  17. machineconfig/scripts/python/{devops_helpers → helpers_devops}/devops_update_repos.py +1 -1
  18. machineconfig/scripts/python/helpers_navigator/__init__.py +20 -0
  19. machineconfig/scripts/python/{helper_navigator → helpers_navigator}/command_builder.py +1 -1
  20. machineconfig/scripts/python/{helper_navigator → helpers_navigator}/command_detail.py +1 -1
  21. machineconfig/scripts/python/{helper_navigator → helpers_navigator}/command_tree.py +1 -1
  22. machineconfig/scripts/python/{helper_navigator → helpers_navigator}/main_app.py +5 -5
  23. machineconfig/scripts/python/{repos_helpers → helpers_repos}/action.py +1 -1
  24. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +11 -6
  25. machineconfig/scripts/python/{repos_helpers → helpers_repos}/count_lines_frontend.py +2 -2
  26. machineconfig/scripts/python/{repos_helpers → helpers_repos}/entrypoint.py +2 -2
  27. machineconfig/scripts/python/interactive.py +3 -3
  28. machineconfig/scripts/python/nw/mount_nfs +1 -1
  29. machineconfig/scripts/python/sessions.py +1 -1
  30. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  31. machineconfig/setup_linux/web_shortcuts/interactive.sh +1 -1
  32. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +1 -1
  33. machineconfig/utils/code.py +1 -1
  34. machineconfig/utils/meta.py +132 -41
  35. machineconfig/utils/procs.py +10 -23
  36. machineconfig/utils/scheduler.py +6 -19
  37. machineconfig/utils/ssh.py +1 -1
  38. {machineconfig-6.47.dist-info → machineconfig-6.49.dist-info}/METADATA +1 -1
  39. {machineconfig-6.47.dist-info → machineconfig-6.49.dist-info}/RECORD +71 -72
  40. machineconfig/scripts/python/helper_navigator/__init__.py +0 -20
  41. machineconfig/scripts/python/sessions_helpers/__init__.py +0 -0
  42. /machineconfig/scripts/python/{cloud_helpers → helpers_cloud}/__init__.py +0 -0
  43. /machineconfig/scripts/python/{cloud_helpers → helpers_cloud}/cloud_helpers.py +0 -0
  44. /machineconfig/scripts/python/{cloud_helpers → helpers_cloud}/cloud_mount.py +0 -0
  45. /machineconfig/scripts/python/{cloud_helpers → helpers_cloud}/helpers5.py +0 -0
  46. /machineconfig/scripts/python/{croshell_helpers → helpers_croshell}/__init__.py +0 -0
  47. /machineconfig/scripts/python/{croshell_helpers → helpers_croshell}/crosh.py +0 -0
  48. /machineconfig/scripts/python/{croshell_helpers → helpers_croshell}/pomodoro.py +0 -0
  49. /machineconfig/scripts/python/{croshell_helpers → helpers_croshell}/scheduler.py +0 -0
  50. /machineconfig/scripts/python/{croshell_helpers → helpers_croshell}/start_slidev.py +0 -0
  51. /machineconfig/scripts/python/{croshell_helpers → helpers_croshell}/viewer.py +0 -0
  52. /machineconfig/scripts/python/{croshell_helpers → helpers_croshell}/viewer_template.py +0 -0
  53. /machineconfig/scripts/python/{devops_helpers → helpers_devops}/__init__.py +0 -0
  54. /machineconfig/scripts/python/{devops_helpers → helpers_devops}/cli_config_dotfile.py +0 -0
  55. /machineconfig/scripts/python/{devops_helpers → helpers_devops}/cli_share_server.py +0 -0
  56. /machineconfig/scripts/python/{devops_helpers → helpers_devops}/cli_terminal.py +0 -0
  57. /machineconfig/scripts/python/{devops_helpers → helpers_devops}/cli_utils.py +0 -0
  58. /machineconfig/scripts/python/{devops_helpers → helpers_devops}/devops_status.py +0 -0
  59. /machineconfig/scripts/python/{devops_helpers → helpers_devops}/themes/__init__.py +0 -0
  60. /machineconfig/scripts/python/{devops_helpers → helpers_devops}/themes/choose_pwsh_theme.ps1 +0 -0
  61. /machineconfig/scripts/python/{devops_helpers → helpers_devops}/themes/choose_wezterm_theme.py +0 -0
  62. /machineconfig/scripts/python/{helper_navigator → helpers_navigator}/data_models.py +0 -0
  63. /machineconfig/scripts/python/{helper_navigator → helpers_navigator}/search_bar.py +0 -0
  64. /machineconfig/scripts/python/{repos_helpers → helpers_repos}/clone.py +0 -0
  65. /machineconfig/scripts/python/{repos_helpers → helpers_repos}/count_lines.py +0 -0
  66. /machineconfig/scripts/python/{repos_helpers → helpers_repos}/record.py +0 -0
  67. /machineconfig/scripts/python/{repos_helpers → helpers_repos}/sync.py +0 -0
  68. /machineconfig/scripts/python/{repos_helpers → helpers_repos}/update.py +0 -0
  69. /machineconfig/scripts/python/{helpers_repos → helpers_sessions}/__init__.py +0 -0
  70. /machineconfig/scripts/python/{sessions_helpers → helpers_sessions}/sessions_multiprocess.py +0 -0
  71. {machineconfig-6.47.dist-info → machineconfig-6.49.dist-info}/WHEEL +0 -0
  72. {machineconfig-6.47.dist-info → machineconfig-6.49.dist-info}/entry_points.txt +0 -0
  73. {machineconfig-6.47.dist-info → machineconfig-6.49.dist-info}/top_level.txt +0 -0
@@ -3,11 +3,14 @@
3
3
  import ast
4
4
  import inspect
5
5
  import textwrap
6
+ from collections.abc import Callable, Mapping
6
7
  from types import FunctionType, ModuleType
7
- from typing import Any
8
+ from typing import ParamSpec
8
9
 
10
+ P = ParamSpec("P")
9
11
 
10
- def function_to_script(func: FunctionType, call_with_args: tuple[Any, ...] | None = None, call_with_kwargs: dict[str, Any] | None = None) -> str:
12
+
13
+ def function_to_script(func: Callable[P, object], call_with_kwargs: Mapping[str, object] | None) -> str:
11
14
  """Convert a function to a standalone executable Python script.
12
15
 
13
16
  This function analyzes a given function and generates a complete Python script
@@ -16,7 +19,6 @@ def function_to_script(func: FunctionType, call_with_args: tuple[Any, ...] | Non
16
19
 
17
20
  Args:
18
21
  func: The function to convert to a script
19
- call_with_args: Optional tuple of positional arguments to call the function with
20
22
  call_with_kwargs: Optional dict of keyword arguments to call the function with
21
23
 
22
24
  Returns:
@@ -25,35 +27,37 @@ def function_to_script(func: FunctionType, call_with_args: tuple[Any, ...] | Non
25
27
  Raises:
26
28
  ValueError: If the function cannot be inspected or analyzed
27
29
  """
28
- if not callable(func) or not hasattr(func, '__code__'):
29
- raise ValueError(f"Expected a function, got {type(func)}")
30
-
31
- call_with_args = call_with_args or ()
32
- call_with_kwargs = call_with_kwargs or {}
33
-
34
- imports = _extract_imports(func)
35
- globals_needed = _extract_globals(func)
36
- source_code = _get_function_source(func)
37
- call_statement = _generate_call_statement(func, call_with_args, call_with_kwargs)
38
-
30
+ if not isinstance(func, FunctionType):
31
+ raise ValueError(f"""Expected a Python function, got {type(func)}""")
32
+
33
+ python_func = func
34
+
35
+ imports = _extract_imports(python_func)
36
+ globals_needed = _extract_globals(python_func)
37
+ source_code = _get_function_source(python_func).rstrip()
38
+ validated_kwargs = _prepare_call_kwargs(python_func, call_with_kwargs)
39
+ call_statement = _generate_call_statement(python_func, validated_kwargs) if validated_kwargs is not None else None
40
+
39
41
  script_parts: list[str] = []
40
-
42
+
41
43
  if imports:
42
44
  script_parts.append(imports)
43
- script_parts.append("")
44
-
45
+
45
46
  if globals_needed:
47
+ if script_parts:
48
+ script_parts.append("")
46
49
  script_parts.append(globals_needed)
50
+
51
+ if script_parts:
47
52
  script_parts.append("")
48
-
53
+
49
54
  script_parts.append(source_code)
50
- script_parts.append("")
51
-
52
- if call_statement:
55
+
56
+ if call_statement is not None:
53
57
  script_parts.append("")
54
58
  script_parts.append("if __name__ == '__main__':")
55
59
  script_parts.append(f" {call_statement}")
56
-
60
+
57
61
  return "\n".join(script_parts)
58
62
 
59
63
 
@@ -86,6 +90,17 @@ def _extract_imports(func: FunctionType) -> str:
86
90
  if isinstance(node.value, ast.Name):
87
91
  used_names.add(node.value.id)
88
92
 
93
+ for node in ast.walk(tree):
94
+ if isinstance(node, ast.FunctionDef):
95
+ for default in node.args.defaults + node.args.kw_defaults:
96
+ if default is not None:
97
+ for subnode in ast.walk(default):
98
+ if isinstance(subnode, ast.Name):
99
+ used_names.add(subnode.id)
100
+ elif isinstance(subnode, ast.Attribute):
101
+ if isinstance(subnode.value, ast.Name):
102
+ used_names.add(subnode.value.id)
103
+
89
104
  for name in used_names:
90
105
  if name in func_globals:
91
106
  obj = func_globals[name]
@@ -97,7 +112,20 @@ def _extract_imports(func: FunctionType) -> str:
97
112
  else:
98
113
  import_statements.add(f"import {module_name} as {name}")
99
114
 
100
- elif hasattr(obj, '__module__') and obj.__module__ != '__main__':
115
+ elif isinstance(obj, type) and hasattr(obj, '__module__') and obj.__module__ != '__main__':
116
+ try:
117
+ module_name = obj.__module__
118
+ obj_name = obj.__name__ if hasattr(obj, '__name__') else name
119
+
120
+ if module_name and module_name != 'builtins':
121
+ if obj_name == name:
122
+ import_statements.add(f"from {module_name} import {obj_name}")
123
+ else:
124
+ import_statements.add(f"from {module_name} import {obj_name} as {name}")
125
+ except AttributeError:
126
+ pass
127
+
128
+ elif callable(obj) and not isinstance(obj, type) and hasattr(obj, '__module__') and obj.__module__ != '__main__':
101
129
  try:
102
130
  module_name = obj.__module__
103
131
  obj_name = obj.__name__ if hasattr(obj, '__name__') else name
@@ -116,6 +144,7 @@ def _extract_imports(func: FunctionType) -> str:
116
144
  def _extract_globals(func: FunctionType) -> str:
117
145
  """Extract global variables needed by the function."""
118
146
  global_assignments: list[str] = []
147
+ needed_types: set[type] = set()
119
148
 
120
149
  source = _get_function_source(func)
121
150
  func_globals = func.__globals__
@@ -133,6 +162,17 @@ def _extract_globals(func: FunctionType) -> str:
133
162
  if isinstance(node.value, ast.Name):
134
163
  used_names.add(node.value.id)
135
164
 
165
+ for node in ast.walk(tree):
166
+ if isinstance(node, ast.FunctionDef):
167
+ for default in node.args.defaults + node.args.kw_defaults:
168
+ if default is not None:
169
+ for subnode in ast.walk(default):
170
+ if isinstance(subnode, ast.Name):
171
+ used_names.add(subnode.id)
172
+ elif isinstance(subnode, ast.Attribute):
173
+ if isinstance(subnode.value, ast.Name):
174
+ used_names.add(subnode.value.id)
175
+
136
176
  for name in used_names:
137
177
  if name in func_globals:
138
178
  obj = func_globals[name]
@@ -140,27 +180,78 @@ def _extract_globals(func: FunctionType) -> str:
140
180
  if not isinstance(obj, (ModuleType, FunctionType, type)):
141
181
  if not (hasattr(obj, '__module__') and hasattr(obj, '__name__')):
142
182
  try:
143
- repr_str = repr(obj)
144
- if len(repr_str) < 1000 and '\n' not in repr_str:
145
- global_assignments.append(f"{name} = {repr_str}")
146
- except Exception:
147
- global_assignments.append(f"# Warning: Could not serialize global variable '{name}'")
148
-
149
- return "\n".join(global_assignments)
150
-
151
-
152
- def _generate_call_statement(func: FunctionType, args: tuple[Any, ...], kwargs: dict[str, Any]) -> str:
153
- """Generate a function call statement with the given arguments."""
154
- if not args and not kwargs:
155
- return f"{func.__name__}()"
183
+ obj_type = type(obj)
184
+
185
+ if obj_type.__name__ == 'PathExtended' and obj_type.__module__ == 'machineconfig.utils.path_extended':
186
+ global_assignments.append(f"{name} = PathExtended('{str(obj)}')")
187
+ if obj_type.__module__ not in ['builtins', '__main__']:
188
+ needed_types.add(obj_type)
189
+ else:
190
+ repr_str = repr(obj)
191
+ if len(repr_str) < 1000 and '\n' not in repr_str and all(ord(c) < 128 or c in [' ', '\n', '\t'] for c in repr_str):
192
+ global_assignments.append(f"{name} = {repr_str}")
193
+ if obj_type.__module__ not in ['builtins', '__main__']:
194
+ needed_types.add(obj_type)
195
+ else:
196
+ global_assignments.append(f"# Warning: Could not serialize global variable '{name}' (repr too complex or contains non-ASCII)")
197
+ except Exception as e:
198
+ global_assignments.append(f"# Warning: Could not serialize global variable '{name}': {e}")
156
199
 
157
- arg_parts: list[str] = []
200
+ result_parts: list[str] = []
158
201
 
159
- for arg in args:
160
- arg_parts.append(repr(arg))
202
+ if needed_types:
203
+ for obj_type in sorted(needed_types, key=lambda t: (t.__module__, t.__name__)):
204
+ module_name = obj_type.__module__
205
+ type_name = obj_type.__name__
206
+ result_parts.append(f"from {module_name} import {type_name}")
207
+ result_parts.append("")
161
208
 
162
- for key, value in kwargs.items():
163
- arg_parts.append(f"{key}={repr(value)}")
209
+ result_parts.extend(global_assignments)
164
210
 
211
+ return "\n".join(result_parts)
212
+
213
+
214
+ def _prepare_call_kwargs(func: FunctionType, call_with_kwargs: Mapping[str, object] | None) -> dict[str, object] | None:
215
+ if call_with_kwargs is None:
216
+ return None
217
+
218
+ normalized_kwargs = dict(call_with_kwargs)
219
+
220
+ if not normalized_kwargs:
221
+ return {}
222
+
223
+ signature = inspect.signature(func)
224
+ positional_only = [parameter.name for parameter in signature.parameters.values() if parameter.kind is inspect.Parameter.POSITIONAL_ONLY]
225
+
226
+ if positional_only:
227
+ joined = ", ".join(positional_only)
228
+ raise ValueError(f"""Cannot call {func.__name__} with positional-only parameters: {joined}""")
229
+
230
+ try:
231
+ signature.bind(**normalized_kwargs)
232
+ except TypeError as error:
233
+ raise ValueError(f"""Invalid call_with_kwargs for {func.__name__}: {error}""") from error
234
+
235
+ return normalized_kwargs
236
+
237
+
238
+ def _generate_call_statement(func: FunctionType, kwargs: dict[str, object]) -> str:
239
+ """Generate a function call statement with the given keyword arguments."""
240
+ if not kwargs:
241
+ return f"{func.__name__}()"
242
+
243
+ arg_parts: list[str] = [f"{key}={repr(value)}" for key, value in kwargs.items()]
165
244
  args_str = ", ".join(arg_parts)
166
245
  return f"{func.__name__}({args_str})"
246
+
247
+
248
+ if __name__ == "__main__":
249
+ # Example usage
250
+ a = True
251
+ b = 3
252
+ def func(no_copy_assets: bool = a):
253
+ from machineconfig.scripts.python.helpers_devops.cli_self import update
254
+ update(no_copy_assets=no_copy_assets)
255
+ script = function_to_script(func, call_with_kwargs=None)
256
+ print(script)
257
+ pass
@@ -106,51 +106,45 @@ class ProcessManager:
106
106
  """Format process data as table string for display."""
107
107
  if not self.data:
108
108
  return ""
109
-
110
109
  # Create header
111
110
  _headers = ["Command", "PID", "Name", "Username", "CPU%", "Memory(MB)", "Status", "Create Time"]
112
111
  header_line = f"{'Command':<50} {'PID':<8} {'Name':<20} {'Username':<12} {'CPU%':<8} {'Memory(MB)':<12} {'Status':<12} {'Create Time':<20}"
113
112
  separator = "-" * len(header_line)
114
-
115
113
  lines = [header_line, separator]
116
-
117
114
  for process in self.data:
118
115
  # Format create_time as string
119
116
  create_time_str = process["create_time"].strftime("%Y-%m-%d %H:%M:%S")
120
117
  # Truncate command if too long
121
118
  command = process["command"][:47] + "..." if len(process["command"]) > 50 else process["command"]
122
-
123
119
  line = f"{command:<50} {process['pid']:<8} {process['name'][:19]:<20} {process['username'][:11]:<12} {process['cpu_percent']:<8.1f} {process['memory_usage_mb']:<12.2f} {process['status'][:11]:<12} {create_time_str:<20}"
124
120
  lines.append(line)
125
-
126
121
  return "\n".join(lines)
127
122
 
128
123
  def choose_and_kill(self):
129
124
  # header for interactive process selection
130
125
  title = "🎯 INTERACTIVE PROCESS SELECTION AND TERMINATION"
131
126
  console.print(Panel(title, title="[bold blue]Process Info[/bold blue]", border_style="blue"))
132
-
133
127
  # Format data as table for display
134
128
  formatted_data = self._format_process_table()
135
- options = formatted_data.split("\n")[1:] # Skip header
136
- res = choose_from_options(options=formatted_data.split("\n"), msg="📋 Select processes to manage:", fzf=True, multi=True)
137
- indices = [options.index(val) for val in res]
129
+ all_lines = formatted_data.split("\n")
130
+ header_and_separator = all_lines[:2] # First two lines: header and separator
131
+ options = all_lines[2:] # Skip header and separator, only process lines
132
+ res = choose_from_options(options=all_lines, msg="📋 Select processes to manage:", fzf=True, multi=True)
133
+ # Filter out header and separator if they were selected
134
+ selected_lines = [line for line in res if line not in header_and_separator]
135
+ indices = [options.index(val) for val in selected_lines]
138
136
  selected_processes = [self.data[i] for i in indices]
139
-
140
137
  print("\n📊 All Processes:")
141
138
  print(formatted_data)
142
139
  print("\n🎯 Selected Processes:")
143
140
  for process in selected_processes:
144
141
  print(f"PID: {process['pid']}, Name: {process['name']}, Memory: {process['memory_usage_mb']:.2f}MB")
145
-
146
142
  for idx, process in enumerate(selected_processes):
147
143
  pprint(dict(process), f"📌 Process {idx}")
148
-
149
144
  kill_all = input("\n⚠️ Confirm killing ALL selected processes? y/[n] ").lower() == "y"
150
145
  if kill_all:
151
146
  self.kill(pids=[p["pid"] for p in selected_processes])
152
147
  return
153
-
154
148
  kill_by_index = input("\n🔫 Kill by index? (enter numbers separated by spaces, e.g. '1 4') or [n] to cancel: ")
155
149
  if kill_by_index != "" and kill_by_index != "n":
156
150
  indices = [int(val) for val in kill_by_index.split(" ")]
@@ -164,12 +158,10 @@ class ProcessManager:
164
158
  # header for filtering processes by name
165
159
  title = "🔍 FILTERING AND TERMINATING PROCESSES BY NAME"
166
160
  console.print(Panel(title, title="[bold blue]Process Info[/bold blue]", border_style="blue"))
167
-
168
161
  # Filter processes by name
169
162
  filtered_processes = [p for p in self.data if p["name"] == name]
170
163
  # Sort by create_time (ascending)
171
164
  filtered_processes.sort(key=lambda x: x["create_time"])
172
-
173
165
  print(f"🎯 Found {len(filtered_processes)} processes matching name: '{name}'")
174
166
  self.kill(pids=[p["pid"] for p in filtered_processes])
175
167
  console.print(Panel("", title="[bold blue]Process Info[/bold blue]", border_style="blue"))
@@ -186,40 +178,35 @@ class ProcessManager:
186
178
  pids = []
187
179
  if commands is None:
188
180
  commands = []
189
-
190
181
  killed_count = 0
191
-
192
182
  for name in names:
193
183
  matching_processes = [p for p in self.data if p["name"] == name]
194
184
  if len(matching_processes) > 0:
195
185
  for process in matching_processes:
196
186
  psutil.Process(process["pid"]).kill()
197
- print(f"💀 Killed process {name} with PID {process['pid']}. It lived {get_age(process['create_time'])}. RIP 🪦💐")
187
+ print(f"💀 Killed process {name} with PID {process['pid']}. It lived {get_age(process['create_time'])}. RIP 💐")
198
188
  killed_count += 1
199
189
  else:
200
190
  print(f'❓ No process named "{name}" found')
201
-
202
191
  for pid in pids:
203
192
  try:
204
193
  proc = psutil.Process(pid)
205
194
  proc_name = proc.name()
206
195
  proc_lifetime = get_age(datetime.fromtimestamp(proc.create_time(), tz=None))
207
196
  proc.kill()
208
- print(f'💀 Killed process with PID {pid} and name "{proc_name}". It lived {proc_lifetime}. RIP 🪦💐')
197
+ print(f'💀 Killed process with PID {pid} and name "{proc_name}". It lived {proc_lifetime}. RIP 💐')
209
198
  killed_count += 1
210
199
  except psutil.NoSuchProcess:
211
200
  print(f"❓ No process with PID {pid} found")
212
-
213
201
  for command in commands:
214
202
  matching_processes = [p for p in self.data if command in p["command"]]
215
203
  if len(matching_processes) > 0:
216
204
  for process in matching_processes:
217
205
  psutil.Process(process["pid"]).kill()
218
- print(f'💀 Killed process with "{command}" in its command & PID {process["pid"]}. It lived {get_age(process["create_time"])}. RIP 🪦💐')
206
+ print(f'💀 Killed process with "{command}" in its command & PID {process["pid"]}. It lived {get_age(process["create_time"])}. RIP 💐')
219
207
  killed_count += 1
220
208
  else:
221
209
  print(f'❓ No process has "{command}" in its command.')
222
-
223
210
  console.print(Panel(f"✅ Termination complete: {killed_count} processes terminated", title="[bold blue]Process Info[/bold blue]", border_style="blue"))
224
211
 
225
212
 
@@ -1,10 +1,10 @@
1
+
1
2
  from pathlib import Path
2
3
  from typing import Callable, Optional, Union, Any, Protocol, List, TypeVar
3
4
  import logging
4
5
  import time
5
6
  from datetime import datetime, timezone, timedelta
6
7
  from machineconfig.utils.io import from_pickle
7
- from machineconfig.utils.path_extended import PathExtended
8
8
 
9
9
 
10
10
  class LoggerTemplate(Protocol):
@@ -147,7 +147,6 @@ class Scheduler:
147
147
  T2 = TypeVar("T2")
148
148
  def to_pickle(obj: Any, path: Path) -> None:
149
149
  import pickle
150
- path.parent.mkdir(parents=True, exist_ok=True)
151
150
  path.write_bytes(pickle.dumps(obj))
152
151
 
153
152
 
@@ -160,15 +159,12 @@ class CacheMemory[T]():
160
159
  self.time_produced = datetime.now()
161
160
  self.logger = logger
162
161
  self.expire = expire
163
- self.name = name if isinstance(name, str) else str(self.source_func)
164
- self.last_call_is_fresh = False
165
-
162
+ self.name = name if isinstance(name, str) else self.source_func.__name__
166
163
  @property
167
164
  def age(self) -> timedelta:
168
165
  return datetime.now() - self.time_produced
169
166
 
170
167
  def __call__(self, fresh: bool = False) -> T:
171
- self.last_call_is_fresh = False
172
168
  if fresh or not hasattr(self, "cache"):
173
169
  why = "There was an explicit fresh order." if fresh else "Previous cache never existed."
174
170
  t0 = time.time()
@@ -177,7 +173,6 @@ class CacheMemory[T]():
177
173
  ℹ️ Reason: {why}""")
178
174
  self.cache = self.source_func()
179
175
  self.logger.warning(f"⏱️ Cache population took {time.time() - t0:.2f} seconds.")
180
- self.last_call_is_fresh = True
181
176
  self.time_produced = datetime.now()
182
177
  else:
183
178
  age = self.age
@@ -189,7 +184,6 @@ class CacheMemory[T]():
189
184
  t0 = time.time()
190
185
  self.cache = self.source_func()
191
186
  self.logger.warning(f"⏱️ Cache population took {time.time() - t0:.2f} seconds.")
192
- self.last_call_is_fresh = True
193
187
  self.time_produced = datetime.now()
194
188
  else:
195
189
  self.logger.warning(f"""
@@ -212,21 +206,19 @@ class Cache[T](): # This class helps to accelrate access to latest data coming
212
206
  ) -> None:
213
207
  self.cache: T
214
208
  self.source_func = source_func # function which when called returns a fresh object to be frozen.
215
- self.path: PathExtended = PathExtended(path)
209
+ self.path: Path = path
210
+ _ = self.path.parent.mkdir(parents=True, exist_ok=True)
216
211
  self.time_produced = datetime.now() # if path is None else
217
212
  self.save = saver
218
213
  self.reader = reader
219
214
  self.logger = logger
220
215
  self.expire = expire
221
- self.name = name if isinstance(name, str) else str(self.source_func)
222
- self.last_call_is_fresh = False
223
-
216
+ self.name = name if isinstance(name, str) else self.source_func.__name__
224
217
  @property
225
218
  def age(self):
226
219
  """Throws AttributeError if called before cache is populated and path doesn't exists"""
227
220
  return datetime.now() - self.time_produced
228
221
  def __call__(self, fresh: bool = False) -> T:
229
- self.last_call_is_fresh = False
230
222
  if fresh or not hasattr(self, "cache"): # populate cache for the first time
231
223
  if not fresh and self.path.exists():
232
224
  age = datetime.now() - datetime.fromtimestamp(self.path.stat().st_mtime)
@@ -245,10 +237,8 @@ class Cache[T](): # This class helps to accelrate access to latest data coming
245
237
  t0 = time.time()
246
238
  self.cache = self.source_func()
247
239
  self.logger.warning(f"⏱️ Cache population took {time.time() - t0:.2f} seconds.")
248
- self.last_call_is_fresh = True
249
240
  self.time_produced = datetime.now()
250
- # if self.path is not None:
251
- # self.save(self.cache, self.path)
241
+ self.save(self.cache, self.path)
252
242
  return self.cache
253
243
  return self(fresh=False) # may be the cache is old ==> check that by passing it through the logic again.
254
244
  else:
@@ -261,7 +251,6 @@ class Cache[T](): # This class helps to accelrate access to latest data coming
261
251
  t0 = time.time()
262
252
  self.cache = self.source_func() # fresh data.
263
253
  self.logger.warning(f"⏱️ Cache population took {time.time() - t0:.2f} seconds.")
264
- self.last_call_is_fresh = True
265
254
  self.time_produced = datetime.now()
266
255
  self.save(self.cache, self.path)
267
256
  else: # cache exists
@@ -277,7 +266,6 @@ class Cache[T](): # This class helps to accelrate access to latest data coming
277
266
  t0 = time.time()
278
267
  self.cache = self.source_func()
279
268
  self.logger.warning(f"⏱️ Cache population took {time.time() - t0:.2f} seconds.")
280
- self.last_call_is_fresh = True
281
269
  self.time_produced = datetime.now()
282
270
  self.save(self.cache, self.path)
283
271
  else:
@@ -294,5 +282,4 @@ class Cache[T](): # This class helps to accelrate access to latest data coming
294
282
  def decorator(source_func: Callable[[], T2]) -> Cache["T2"]:
295
283
  res = Cache(source_func=source_func, expire=expire, logger=logger, path=path, name=name, reader=reader, saver=saver)
296
284
  return res
297
-
298
285
  return decorator
@@ -6,7 +6,7 @@ from machineconfig.utils.terminal import Response, MACHINE
6
6
  from machineconfig.utils.accessories import pprint
7
7
 
8
8
  UV_RUN_CMD = "$HOME/.local/bin/uv run"
9
- MACHINECONFIG_VERSION = "machineconfig>=6.47"
9
+ MACHINECONFIG_VERSION = "machineconfig>=6.49"
10
10
  DEFAULT_PICKLE_SUBDIR = "tmp_results/tmp_scripts/ssh"
11
11
 
12
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machineconfig
3
- Version: 6.47
3
+ Version: 6.49
4
4
  Summary: Dotfiles management package
5
5
  Author-email: Alex Al-Saffar <programmer@usa.com>
6
6
  License: Apache 2.0