ry-tool 1.0.1__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.
ry_tool/context.py ADDED
@@ -0,0 +1,297 @@
1
+ """
2
+ Execution context that holds all variables available to templates and execution.
3
+
4
+ Purpose: Single source of truth for all execution variables.
5
+ Provides flags, arguments, environment, and computed values.
6
+ No execution logic, just data management.
7
+ """
8
+ import os
9
+ from dataclasses import dataclass, field
10
+ from typing import Dict, Any, List, Optional
11
+ from pathlib import Path
12
+
13
+
14
+ @dataclass
15
+ class ExecutionContext:
16
+ """
17
+ Complete context for command execution.
18
+
19
+ This is what templates and code blocks have access to.
20
+ """
21
+ # From parsed command
22
+ command: str = ""
23
+ subcommand: Optional[str] = None
24
+ flags: Dict[str, Any] = field(default_factory=dict)
25
+ arguments: Dict[str, Any] = field(default_factory=dict) # Named arguments
26
+ positionals: List[str] = field(default_factory=list) # Unnamed positionals
27
+ remaining: List[str] = field(default_factory=list) # After --
28
+ remaining_args: List[str] = field(default_factory=list) # All args for relay
29
+
30
+ # From environment
31
+ env: Dict[str, str] = field(default_factory=dict)
32
+ cwd: Path = field(default_factory=Path.cwd)
33
+
34
+ # From library
35
+ library_name: str = ""
36
+ library_version: str = ""
37
+ library_path: Optional[Path] = None
38
+ target: Optional[str] = None # Native command path
39
+
40
+ # Runtime values
41
+ captured: Dict[str, str] = field(default_factory=dict) # Captured variables
42
+
43
+ def __post_init__(self):
44
+ """Initialize with current environment."""
45
+ if not self.env:
46
+ self.env = dict(os.environ)
47
+
48
+ def get(self, path: str, default: Any = None) -> Any:
49
+ """
50
+ Get value by dot-notation path.
51
+
52
+ Examples:
53
+ ctx.get('flags.message')
54
+ ctx.get('env.USER')
55
+ ctx.get('arguments.branch', 'main')
56
+ """
57
+ parts = path.split('.')
58
+ value = self
59
+
60
+ for part in parts:
61
+ if hasattr(value, part):
62
+ value = getattr(value, part)
63
+ elif isinstance(value, dict):
64
+ value = value.get(part)
65
+ if value is None:
66
+ return default
67
+ elif isinstance(value, list):
68
+ try:
69
+ index = int(part)
70
+ value = value[index] if index < len(value) else default
71
+ except (ValueError, IndexError):
72
+ return default
73
+ else:
74
+ return default
75
+
76
+ return value
77
+
78
+ def set(self, path: str, value: Any):
79
+ """
80
+ Set value by dot-notation path.
81
+
82
+ Examples:
83
+ ctx.set('flags.token', 'abc123')
84
+ ctx.set('captured.BUILD_TOKEN', 'xyz')
85
+ """
86
+ parts = path.split('.')
87
+ target = self
88
+
89
+ # Navigate to parent
90
+ for part in parts[:-1]:
91
+ if hasattr(target, part):
92
+ target = getattr(target, part)
93
+ elif isinstance(target, dict):
94
+ if part not in target:
95
+ target[part] = {}
96
+ target = target[part]
97
+
98
+ # Set the value
99
+ last_part = parts[-1]
100
+ if hasattr(target, last_part):
101
+ setattr(target, last_part, value)
102
+ elif isinstance(target, dict):
103
+ target[last_part] = value
104
+
105
+ def to_dict(self) -> Dict[str, Any]:
106
+ """
107
+ Convert context to dictionary for template rendering.
108
+
109
+ Returns flat and nested versions for convenience.
110
+ """
111
+ return {
112
+ # Direct access
113
+ 'command': self.command,
114
+ 'subcommand': self.subcommand,
115
+ 'flags': self.flags,
116
+ 'arguments': self.arguments,
117
+ 'positionals': self.positionals,
118
+ 'remaining': self.remaining,
119
+ 'remaining_args': self.remaining_args, # Full args for relay
120
+ 'env': self.env,
121
+ 'cwd': str(self.cwd),
122
+ 'library_name': self.library_name,
123
+ 'library_version': self.library_version,
124
+ 'captured': self.captured,
125
+
126
+ # Computed values
127
+ 'original': self._reconstruct_original(),
128
+ 'relay': self._build_relay_command(),
129
+ 'relay_base': self.target or self.command,
130
+ }
131
+
132
+ def _reconstruct_original(self) -> str:
133
+ """Reconstruct original command line."""
134
+ parts = [self.command]
135
+
136
+ if self.subcommand:
137
+ parts.append(self.subcommand)
138
+
139
+ # Add flags
140
+ for key, value in self.flags.items():
141
+ if len(key) == 1:
142
+ parts.append(f'-{key}')
143
+ else:
144
+ parts.append(f'--{key}')
145
+
146
+ if value is not True: # Not a boolean flag
147
+ parts.append(str(value))
148
+
149
+ # Add positionals
150
+ parts.extend(self.positionals)
151
+
152
+ # Add remaining after --
153
+ if self.remaining:
154
+ parts.append('--')
155
+ parts.extend(self.remaining)
156
+
157
+ return ' '.join(parts)
158
+
159
+ def _build_relay_command(self) -> str:
160
+ """Build command for relaying to native tool."""
161
+ if not self.target:
162
+ return self._reconstruct_original()
163
+
164
+ parts = [self.target, self.command]
165
+
166
+ if self.subcommand:
167
+ parts.append(self.subcommand)
168
+
169
+ # Add all flags and args as-is
170
+ for key, value in self.flags.items():
171
+ if len(key) == 1:
172
+ parts.append(f'-{key}')
173
+ else:
174
+ parts.append(f'--{key}')
175
+
176
+ if value is not True:
177
+ parts.append(str(value))
178
+
179
+ parts.extend(self.positionals)
180
+
181
+ if self.remaining:
182
+ parts.append('--')
183
+ parts.extend(self.remaining)
184
+
185
+ return ' '.join(parts)
186
+
187
+ def rebuild_remaining_args(self) -> List[str]:
188
+ """
189
+ Rebuild remaining_args from current flag/argument values.
190
+ This is needed after before hooks modify flags.
191
+ """
192
+ args = []
193
+
194
+ # Add command
195
+ if self.command:
196
+ args.append(self.command)
197
+
198
+ # Add subcommand if present
199
+ if self.subcommand:
200
+ args.append(self.subcommand)
201
+
202
+ # Add positionals before flags (typical order)
203
+ args.extend(self.positionals)
204
+
205
+ # Add flags with current values
206
+ for key, value in self.flags.items():
207
+ if len(key) == 1:
208
+ args.append(f'-{key}')
209
+ else:
210
+ args.append(f'--{key}')
211
+
212
+ if value is not True:
213
+ args.append(str(value))
214
+
215
+ # Add remaining after --
216
+ if self.remaining:
217
+ args.append('--')
218
+ args.extend(self.remaining)
219
+
220
+ return args
221
+
222
+ def apply_modifications(self, mods: Dict[str, Any]):
223
+ """
224
+ Apply modifications from execution steps.
225
+
226
+ This is the central method for updating context state after
227
+ execution steps that modify flags, arguments, or environment.
228
+
229
+ Args:
230
+ mods: Dictionary of modifications to apply
231
+ Keys can be: flags, arguments, env, captured, positionals
232
+ """
233
+ if not mods:
234
+ return
235
+
236
+ # Apply flag modifications
237
+ if 'flags' in mods:
238
+ # Update flags with new values
239
+ if isinstance(mods['flags'], dict):
240
+ self.flags.update(mods['flags'])
241
+ else:
242
+ self.flags = mods['flags']
243
+ # Rebuild remaining_args to reflect flag changes
244
+ self.remaining_args = self.rebuild_remaining_args()
245
+
246
+ # Apply argument modifications
247
+ if 'arguments' in mods:
248
+ if isinstance(mods['arguments'], dict):
249
+ self.arguments.update(mods['arguments'])
250
+ else:
251
+ self.arguments = mods['arguments']
252
+
253
+ # Apply environment modifications
254
+ if 'env' in mods:
255
+ if isinstance(mods['env'], dict):
256
+ self.env.update(mods['env'])
257
+ else:
258
+ self.env = mods['env']
259
+
260
+ # Apply captured variable modifications
261
+ if 'captured' in mods:
262
+ if isinstance(mods['captured'], dict):
263
+ self.captured.update(mods['captured'])
264
+ else:
265
+ self.captured = mods['captured']
266
+
267
+ # Apply positional modifications
268
+ if 'positionals' in mods:
269
+ self.positionals = mods['positionals']
270
+ # Rebuild remaining_args if positionals changed
271
+ self.remaining_args = self.rebuild_remaining_args()
272
+
273
+ # Apply remaining modifications
274
+ if 'remaining' in mods:
275
+ self.remaining = mods['remaining']
276
+ # Rebuild remaining_args if remaining changed
277
+ self.remaining_args = self.rebuild_remaining_args()
278
+
279
+ def copy(self) -> 'ExecutionContext':
280
+ """Create a deep copy of the context."""
281
+ return ExecutionContext(
282
+ command=self.command,
283
+ subcommand=self.subcommand,
284
+ flags=self.flags.copy(),
285
+ arguments=self.arguments.copy(),
286
+ positionals=self.positionals.copy(),
287
+ remaining=self.remaining.copy(),
288
+ remaining_args=self.remaining_args.copy(),
289
+ env=self.env.copy(),
290
+ cwd=self.cwd,
291
+ library_name=self.library_name,
292
+ library_version=self.library_version,
293
+ library_path=self.library_path,
294
+ target=self.target,
295
+ captured=self.captured.copy()
296
+ )
297
+