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/__init__.py +27 -0
- ry_tool/__main__.py +9 -0
- ry_tool/_cli.py +244 -0
- ry_tool/app.py +420 -0
- ry_tool/context.py +297 -0
- ry_tool/executor.py +475 -0
- ry_tool/installer.py +176 -0
- ry_tool/loader.py +280 -0
- ry_tool/matcher.py +233 -0
- ry_tool/parser.py +248 -0
- ry_tool/template.py +306 -0
- ry_tool/utils.py +396 -0
- ry_tool-1.0.1.dist-info/METADATA +112 -0
- ry_tool-1.0.1.dist-info/RECORD +16 -0
- ry_tool-1.0.1.dist-info/WHEEL +4 -0
- ry_tool-1.0.1.dist-info/entry_points.txt +3 -0
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
|
+
|