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/utils.py ADDED
@@ -0,0 +1,396 @@
1
+ """
2
+ Shared utilities for reducing boilerplate across ry-next modules.
3
+
4
+ Contains common patterns for error handling, subprocess execution,
5
+ file operations, and other repetitive tasks.
6
+ """
7
+ import os
8
+ import sys
9
+ import json
10
+ import subprocess
11
+ import functools
12
+ from pathlib import Path
13
+ from typing import Dict, Any, List, Optional, Tuple, Union
14
+ from datetime import datetime, date
15
+ import yaml
16
+
17
+
18
+ def handle_errors(return_on_error=False, print_prefix="❌"):
19
+ """
20
+ Decorator for consistent error handling across library modules.
21
+
22
+ Args:
23
+ return_on_error: Value to return on error (False for bool functions)
24
+ print_prefix: Prefix for error messages
25
+ """
26
+ def decorator(func):
27
+ @functools.wraps(func)
28
+ def wrapper(*args, **kwargs):
29
+ try:
30
+ return func(*args, **kwargs)
31
+ except Exception as e:
32
+ print(f"{print_prefix} {func.__name__} failed: {e}", file=sys.stderr)
33
+ return return_on_error
34
+ return wrapper
35
+ return decorator
36
+
37
+
38
+ class CommandBuilder:
39
+ """Build subprocess commands with consistent error handling."""
40
+
41
+ def __init__(self, capture_output=True, text=True, check=False):
42
+ self.capture_output = capture_output
43
+ self.text = text
44
+ self.check = check
45
+ self.env = None
46
+ self.cwd = None
47
+
48
+ def with_env(self, env: Dict[str, str]) -> 'CommandBuilder':
49
+ """Add environment variables."""
50
+ self.env = env
51
+ return self
52
+
53
+ def with_cwd(self, cwd: Union[str, Path]) -> 'CommandBuilder':
54
+ """Set working directory."""
55
+ self.cwd = str(cwd)
56
+ return self
57
+
58
+ def run(self, cmd: List[str]) -> subprocess.CompletedProcess:
59
+ """Execute command with configured options."""
60
+ kwargs = {
61
+ 'capture_output': self.capture_output,
62
+ 'text': self.text,
63
+ 'check': self.check
64
+ }
65
+
66
+ if self.env:
67
+ exec_env = os.environ.copy()
68
+ exec_env.update(self.env)
69
+ kwargs['env'] = exec_env
70
+
71
+ if self.cwd:
72
+ kwargs['cwd'] = self.cwd
73
+
74
+ return subprocess.run(cmd, **kwargs)
75
+
76
+ def run_git(self, *args) -> subprocess.CompletedProcess:
77
+ """Execute git command."""
78
+ return self.run(['/usr/bin/git'] + list(args))
79
+
80
+ def run_uv(self, *args) -> subprocess.CompletedProcess:
81
+ """Execute uv command."""
82
+ return self.run(['/usr/bin/uv'] + list(args))
83
+
84
+
85
+ class FileManager:
86
+ """Manage file operations with consistent error handling and validation."""
87
+
88
+ @staticmethod
89
+ @handle_errors(return_on_error=None)
90
+ def load_yaml(path: Path) -> Optional[Dict[str, Any]]:
91
+ """Load YAML file with error handling."""
92
+ if not path.exists():
93
+ raise FileNotFoundError(f"File not found: {path}")
94
+
95
+ with open(path) as f:
96
+ return yaml.safe_load(f)
97
+
98
+ @staticmethod
99
+ @handle_errors(return_on_error=False)
100
+ def save_yaml(data: Dict[str, Any], path: Path, sort_keys=False) -> bool:
101
+ """Save YAML file with error handling."""
102
+ path.parent.mkdir(parents=True, exist_ok=True)
103
+ with open(path, 'w') as f:
104
+ yaml.dump(data, f, sort_keys=sort_keys, default_flow_style=False)
105
+ return True
106
+
107
+ @staticmethod
108
+ @handle_errors(return_on_error=None)
109
+ def load_json(path: Path) -> Optional[Dict[str, Any]]:
110
+ """Load JSON file with error handling."""
111
+ if not path.exists():
112
+ return {}
113
+
114
+ with open(path) as f:
115
+ return json.load(f)
116
+
117
+ @staticmethod
118
+ @handle_errors(return_on_error=False)
119
+ def save_json(data: Dict[str, Any], path: Path, indent=2) -> bool:
120
+ """Save JSON file with error handling."""
121
+ path.parent.mkdir(parents=True, exist_ok=True)
122
+ with open(path, 'w') as f:
123
+ json.dump(data, f, indent=indent, sort_keys=True)
124
+ return True
125
+
126
+ @staticmethod
127
+ def ensure_dir(path: Path) -> Path:
128
+ """Ensure directory exists and return path."""
129
+ path.mkdir(parents=True, exist_ok=True)
130
+ return path
131
+
132
+
133
+ class VersionManager:
134
+ """Handle semantic version operations."""
135
+
136
+ @staticmethod
137
+ def parse_version(version: str) -> Tuple[int, int, int]:
138
+ """Parse version string into components."""
139
+ parts = version.split('.')
140
+ major = int(parts[0]) if len(parts) > 0 else 0
141
+ minor = int(parts[1]) if len(parts) > 1 else 0
142
+ patch = int(parts[2]) if len(parts) > 2 else 0
143
+ return major, minor, patch
144
+
145
+ @staticmethod
146
+ def bump_version(version: str, bump_type: str) -> str:
147
+ """Bump version by type (major, minor, patch)."""
148
+ major, minor, patch = VersionManager.parse_version(version)
149
+
150
+ if bump_type == 'major':
151
+ major += 1
152
+ minor = patch = 0
153
+ elif bump_type == 'minor':
154
+ minor += 1
155
+ patch = 0
156
+ else: # patch
157
+ patch += 1
158
+
159
+ return f"{major}.{minor}.{patch}"
160
+
161
+ @staticmethod
162
+ def is_valid_version(version: str) -> bool:
163
+ """Check if version string is valid semver."""
164
+ try:
165
+ VersionManager.parse_version(version)
166
+ return True
167
+ except (ValueError, IndexError):
168
+ return False
169
+
170
+
171
+ class LibraryBase:
172
+ """Base class for library operation modules with common functionality."""
173
+
174
+ def __init__(self, base_path: str = 'docs_next/libraries'):
175
+ self.base_path = Path(base_path)
176
+ self.file_manager = FileManager()
177
+ self.version_manager = VersionManager()
178
+ self.cmd = CommandBuilder()
179
+
180
+ def get_library_dir(self, name: str) -> Path:
181
+ """Get library directory path."""
182
+ return self.base_path / name
183
+
184
+ def get_library_yaml(self, name: str) -> Path:
185
+ """Get library YAML file path."""
186
+ return self.get_library_dir(name) / f"{name}.yaml"
187
+
188
+ def get_meta_yaml(self, name: str) -> Path:
189
+ """Get library meta.yaml path."""
190
+ return self.get_library_dir(name) / "meta.yaml"
191
+
192
+ def library_exists(self, name: str) -> bool:
193
+ """Check if library exists."""
194
+ return self.get_library_yaml(name).exists()
195
+
196
+ @handle_errors(return_on_error=None)
197
+ def load_library_config(self, name: str) -> Optional[Dict[str, Any]]:
198
+ """Load library configuration."""
199
+ return self.file_manager.load_yaml(self.get_library_yaml(name))
200
+
201
+ @handle_errors(return_on_error=None)
202
+ def load_library_meta(self, name: str) -> Optional[Dict[str, Any]]:
203
+ """Load library metadata."""
204
+ meta_path = self.get_meta_yaml(name)
205
+ if not meta_path.exists():
206
+ return {}
207
+ return self.file_manager.load_yaml(meta_path)
208
+
209
+ @handle_errors(return_on_error=False)
210
+ def save_library_meta(self, name: str, meta: Dict[str, Any]) -> bool:
211
+ """Save library metadata."""
212
+ meta['updated'] = date.today().isoformat()
213
+ return self.file_manager.save_yaml(meta, self.get_meta_yaml(name))
214
+
215
+ def get_library_version(self, name: str) -> str:
216
+ """Get current library version."""
217
+ meta = self.load_library_meta(name)
218
+ return meta.get('version', '0.0.0') if meta else '0.0.0'
219
+
220
+ def list_libraries(self) -> List[str]:
221
+ """List all available libraries."""
222
+ if not self.base_path.exists():
223
+ return []
224
+
225
+ libraries = []
226
+ for item in self.base_path.iterdir():
227
+ if item.is_dir():
228
+ yaml_file = item / f"{item.name}.yaml"
229
+ if yaml_file.exists():
230
+ libraries.append(item.name)
231
+
232
+ return sorted(libraries)
233
+
234
+ def success_message(self, message: str):
235
+ """Print success message."""
236
+ print(f"✅ {message}")
237
+
238
+ def info_message(self, message: str):
239
+ """Print info message."""
240
+ print(f"ℹ️ {message}")
241
+
242
+ def warning_message(self, message: str):
243
+ """Print warning message."""
244
+ print(f"⚠️ {message}")
245
+
246
+ def error_message(self, message: str):
247
+ """Print error message."""
248
+ print(f"❌ {message}", file=sys.stderr)
249
+
250
+
251
+
252
+
253
+ def validate_name(name: str) -> bool:
254
+ """Validate library/package name format."""
255
+ return name.replace('-', '').replace('_', '').isalnum()
256
+
257
+
258
+ def get_current_date() -> str:
259
+ """Get current date in ISO format."""
260
+ return date.today().isoformat()
261
+
262
+
263
+ def get_current_datetime() -> str:
264
+ """Get current datetime in ISO format."""
265
+ return datetime.now().isoformat()
266
+
267
+
268
+ class ContextFactory:
269
+ """Factory for building execution contexts with consistent patterns."""
270
+
271
+ @staticmethod
272
+ def from_parsed_command(parsed, library, command_config=None):
273
+ """
274
+ Build ExecutionContext from parsed command and library.
275
+
276
+ Centralizes the context building logic that was duplicated
277
+ across matcher.py and app.py.
278
+ """
279
+ from .context import ExecutionContext
280
+
281
+ # For augmentation libraries, use raw args to preserve exact user input
282
+ # This ensures flags like -10 aren't incorrectly parsed as --10
283
+ if library.type == 'augmentation' and parsed.raw_args:
284
+ # Use ALL raw args - they represent the actual git/tool command
285
+ remaining_args = parsed.raw_args
286
+ else:
287
+ # For other library types, reconstruct from parsed components
288
+ # Build remaining_args for relay (all original args)
289
+ remaining_args = []
290
+ if parsed.command:
291
+ remaining_args.append(parsed.command)
292
+ if parsed.subcommand:
293
+ remaining_args.append(parsed.subcommand)
294
+ remaining_args.extend(parsed.positionals)
295
+
296
+ # Add flags in original format
297
+ for flag, value in parsed.flags.items():
298
+ if len(flag) == 1:
299
+ remaining_args.append(f'-{flag}')
300
+ else:
301
+ remaining_args.append(f'--{flag}')
302
+ if value is not True:
303
+ remaining_args.append(str(value))
304
+
305
+ if parsed.remaining:
306
+ remaining_args.append('--')
307
+ remaining_args.extend(parsed.remaining)
308
+
309
+ context = ExecutionContext(
310
+ command=parsed.command,
311
+ subcommand=parsed.subcommand,
312
+ flags=parsed.flags,
313
+ positionals=parsed.positionals,
314
+ remaining=parsed.remaining,
315
+ remaining_args=remaining_args,
316
+ library_name=library.name,
317
+ library_version=library.metadata.get('version', '0.0.0'),
318
+ library_path=library.path,
319
+ target=library.target
320
+ )
321
+
322
+ # Map positionals to named arguments if schema provided
323
+ if command_config and 'arguments' in command_config:
324
+ ContextFactory._map_arguments(context, command_config['arguments'])
325
+
326
+ return context
327
+
328
+ @staticmethod
329
+ def _map_arguments(context, arg_schema):
330
+ """
331
+ Map positional arguments to named arguments based on schema.
332
+
333
+ Args:
334
+ context: ExecutionContext to update
335
+ arg_schema: Argument schema from command config
336
+ """
337
+ positionals = context.positionals.copy()
338
+
339
+ for arg_name, arg_config in arg_schema.items():
340
+ if not positionals:
341
+ # No more positionals to map
342
+ if isinstance(arg_config, dict) and arg_config.get('required'):
343
+ context.arguments[arg_name] = None
344
+ elif arg_config == 'required':
345
+ context.arguments[arg_name] = None
346
+ continue
347
+
348
+ # Map positional to named argument
349
+ if isinstance(arg_config, dict):
350
+ if arg_config.get('multiple'):
351
+ # Consume all remaining positionals
352
+ context.arguments[arg_name] = positionals
353
+ positionals = []
354
+ else:
355
+ # Consume one positional
356
+ context.arguments[arg_name] = positionals.pop(0)
357
+ else:
358
+ # Simple required/optional
359
+ context.arguments[arg_name] = positionals.pop(0)
360
+
361
+ # Update positionals with unmapped ones
362
+ context.positionals = positionals
363
+
364
+ @staticmethod
365
+ def create_library_env(library):
366
+ """
367
+ Create environment variables for library execution.
368
+
369
+ Centralizes the env setup logic from app.py.
370
+ """
371
+ import os
372
+
373
+ env = {
374
+ 'RY_LIBRARY_NAME': library.name,
375
+ 'RY_LIBRARY_VERSION': library.metadata.get('version', '0.0.0'),
376
+ 'RY_LIBRARY_TYPE': library.type,
377
+ }
378
+
379
+ # Set library directory if it's in standard location
380
+ if library.path:
381
+ if library.path.parent.name == library.name:
382
+ # Directory format - has lib/ folder
383
+ env['RY_LIBRARY_DIR'] = str(library.path.parent)
384
+
385
+ # Add lib/ to Python path if it exists
386
+ lib_path = library.path.parent / 'lib'
387
+ if lib_path.exists():
388
+ current_pythonpath = os.environ.get('PYTHONPATH', '')
389
+ env['PYTHONPATH'] = f"{lib_path}:{current_pythonpath}" if current_pythonpath else str(lib_path)
390
+ else:
391
+ # Single file format
392
+ env['RY_LIBRARY_DIR'] = str(library.path.parent)
393
+
394
+ env['RY_LIBRARY_PATH'] = str(library.path)
395
+
396
+ return env
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.3
2
+ Name: ry-tool
3
+ Version: 1.0.1
4
+ Summary: Pure YAML command orchestrator - CI/CD for humans
5
+ Author: Fredrik Angelsen
6
+ Author-email: Fredrik Angelsen <fredrikangelsen@gmail.com>
7
+ Requires-Dist: pyyaml>=6.0
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ # ry-next
12
+
13
+ A clean, modular command augmentation framework that enhances existing CLI tools without breaking their native behavior.
14
+
15
+ ## Features
16
+
17
+ - **Command Augmentation**: Wrap and enhance existing CLI tools
18
+ - **Clean Architecture**: Modular design with single-responsibility components
19
+ - **Type-Safe Processing**: Recursive template processing with type dispatch
20
+ - **Token-Based Safety**: Time-limited tokens for dangerous operations
21
+ - **Library System**: Reusable command definitions with metadata
22
+ - **No Shell Escaping**: Direct subprocess execution for safety
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install -e .
28
+ ```
29
+
30
+ This installs the `ry-next` command globally.
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ # List available libraries
36
+ ry-next --list
37
+
38
+ # Get help for a library
39
+ ry-next git --ry-help
40
+
41
+ # Execute augmented command
42
+ ry-next git commit -m "feat: new feature"
43
+
44
+ # Show execution plan (dry run)
45
+ ry-next --ry-run git commit -m "test"
46
+ ```
47
+
48
+ ## Production Libraries
49
+
50
+ - **git** - Enhanced git workflow with review tokens and commit validation
51
+ - **uv** - Python package management with automated version workflows
52
+ - **changelog** - Simple changelog management following Keep a Changelog
53
+ - **ry-lib** - Library development and management tools
54
+
55
+ ## Documentation
56
+
57
+ - [Full Documentation](docs/README_RYNEXT.md)
58
+ - [Library Development](docs/libraries/ry-lib/README.md)
59
+ - [Examples](examples/README.md)
60
+
61
+ ## Project Structure
62
+
63
+ ```
64
+ ry-next/
65
+ ├── src/ry_next/ # Core implementation
66
+ ├── docs/
67
+ │ ├── libraries/ # Production libraries
68
+ │ └── README_RYNEXT.md # Full documentation
69
+ ├── examples/ # Example libraries
70
+ └── _archive/ # Old ry-tool code (deprecated)
71
+ ```
72
+
73
+ ## Key Concepts
74
+
75
+ ### Library Format (v2.0)
76
+
77
+ ```yaml
78
+ version: "2.0"
79
+ name: git
80
+ type: augmentation
81
+ target: /usr/bin/git
82
+
83
+ commands:
84
+ commit:
85
+ flags:
86
+ m/message: string
87
+ augment:
88
+ before:
89
+ - python: |
90
+ # Validation logic
91
+ relay: native
92
+ ```
93
+
94
+ ### Token-Based Safety
95
+
96
+ Critical operations require preview and token verification:
97
+
98
+ ```bash
99
+ # Preview changes
100
+ git diff --staged # → Generates REVIEW_TOKEN
101
+
102
+ # Execute with token
103
+ REVIEW_TOKEN=xxx git commit -m "message"
104
+ ```
105
+
106
+ ## Development
107
+
108
+ See [docs/README_RYNEXT.md](docs/README_RYNEXT.md) for complete documentation.
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,16 @@
1
+ ry_tool/__init__.py,sha256=xPmIRsgE_swhmHJsxGIjdd8VXQz0MkVoPI1irFBgWJw,652
2
+ ry_tool/__main__.py,sha256=fbQGeI2WWTZrLUI7LToOfAV5JckzB_El64i0zjqOouI,138
3
+ ry_tool/_cli.py,sha256=8gZ-c1FlerAZ5e0K4v7Mv2GafMLiGAcv-3ET8oHmMdQ,7693
4
+ ry_tool/app.py,sha256=TjMCQDZ5QkqKiAqUViGe84T6d6Deb67jnRRFAmEk15w,15997
5
+ ry_tool/context.py,sha256=elDiqty6Fssq-74ra8twtzKfolQ154xLj2ECwZbzcy4,9743
6
+ ry_tool/executor.py,sha256=XiwWKl1YMa0zgok4C6beuxAWDVhEs2i1gL_vxPlNbcA,17199
7
+ ry_tool/installer.py,sha256=cFnEROvpfIjMkpZF4AzRU4bHDNneJBMk5gRXP5jJgqE,5712
8
+ ry_tool/loader.py,sha256=Ce1-M3uJ554_H1M8j1ZFTXPE8dxSvFXR6l88utM9zHc,9809
9
+ ry_tool/matcher.py,sha256=fg857FoF2CjLsUvJl2vaeRHHHGx6A8vraI1fnddOKN4,7458
10
+ ry_tool/parser.py,sha256=c9cNsXf1kdWpmXH3IVdejlePIrZWmJDdnh3mQlAyPmI,9253
11
+ ry_tool/template.py,sha256=uEvc70vA7NZ3qKemsddTnYFKhdc2sEf-5uytd2wPiBk,10094
12
+ ry_tool/utils.py,sha256=TpJz7DSkyJewzyDPUsQXI9awDgMbE6nC__DRbUYNQpw,13762
13
+ ry_tool-1.0.1.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
14
+ ry_tool-1.0.1.dist-info/entry_points.txt,sha256=z2YdpRoOA9Kk0Sg26A6Eu6PrwdJLQ22dxdZqHnhWYNo,45
15
+ ry_tool-1.0.1.dist-info/METADATA,sha256=d2x2ZdUHzX6gHW-zVhJ-4kdu9KBuziKrosqdNms29tE,2612
16
+ ry_tool-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.15
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ ry-next = ry_tool.app:run
3
+