ftl2 0.1.0__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.
Files changed (66) hide show
  1. ftl2/__init__.py +18 -0
  2. ftl2/arguments.py +116 -0
  3. ftl2/automation/__init__.py +275 -0
  4. ftl2/automation/context.py +1769 -0
  5. ftl2/automation/proxy.py +1292 -0
  6. ftl2/backup.py +640 -0
  7. ftl2/builder.py +121 -0
  8. ftl2/cli.py +2127 -0
  9. ftl2/config_profiles.py +258 -0
  10. ftl2/events.py +226 -0
  11. ftl2/exceptions.py +375 -0
  12. ftl2/executor.py +366 -0
  13. ftl2/ftl_gate/__init__.py +7 -0
  14. ftl2/ftl_gate/__main__.py +1125 -0
  15. ftl2/ftl_modules/__init__.py +162 -0
  16. ftl2/ftl_modules/aws/__init__.py +8 -0
  17. ftl2/ftl_modules/aws/ec2.py +33 -0
  18. ftl2/ftl_modules/command.py +141 -0
  19. ftl2/ftl_modules/exceptions.py +57 -0
  20. ftl2/ftl_modules/executor.py +521 -0
  21. ftl2/ftl_modules/file.py +372 -0
  22. ftl2/ftl_modules/http.py +338 -0
  23. ftl2/ftl_modules/pip.py +166 -0
  24. ftl2/ftl_modules/swap.py +230 -0
  25. ftl2/ftl_modules/wait_for.py +115 -0
  26. ftl2/gate.py +496 -0
  27. ftl2/host_filter.py +187 -0
  28. ftl2/inventory.py +422 -0
  29. ftl2/logging.py +403 -0
  30. ftl2/message.py +239 -0
  31. ftl2/module_docs.py +562 -0
  32. ftl2/module_loading/__init__.py +75 -0
  33. ftl2/module_loading/bundle.py +499 -0
  34. ftl2/module_loading/dependencies.py +508 -0
  35. ftl2/module_loading/excluded.py +189 -0
  36. ftl2/module_loading/executor.py +933 -0
  37. ftl2/module_loading/fqcn.py +421 -0
  38. ftl2/module_loading/requirements.py +430 -0
  39. ftl2/module_loading/shadowed.py +58 -0
  40. ftl2/modules/copy.py +146 -0
  41. ftl2/modules/file.py +161 -0
  42. ftl2/modules/ping.py +49 -0
  43. ftl2/modules/setup.py +80 -0
  44. ftl2/modules/shell.py +92 -0
  45. ftl2/policy.py +177 -0
  46. ftl2/progress.py +631 -0
  47. ftl2/refs.py +174 -0
  48. ftl2/retry.py +442 -0
  49. ftl2/runners.py +1302 -0
  50. ftl2/safety.py +219 -0
  51. ftl2/ssh.py +623 -0
  52. ftl2/state/__init__.py +41 -0
  53. ftl2/state/execution.py +304 -0
  54. ftl2/state/file.py +93 -0
  55. ftl2/state/merge.py +64 -0
  56. ftl2/state/state.py +288 -0
  57. ftl2/telemetry.py +73 -0
  58. ftl2/types.py +311 -0
  59. ftl2/utils.py +186 -0
  60. ftl2/vars.py +373 -0
  61. ftl2/vault.py +99 -0
  62. ftl2/workflow.py +314 -0
  63. ftl2-0.1.0.dist-info/METADATA +207 -0
  64. ftl2-0.1.0.dist-info/RECORD +66 -0
  65. ftl2-0.1.0.dist-info/WHEEL +4 -0
  66. ftl2-0.1.0.dist-info/entry_points.txt +3 -0
ftl2/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """FTL2 - Refactored Faster Than Light automation framework.
2
+
3
+ A high-performance automation framework built with modern Python patterns,
4
+ using dataclasses and composition for clean architecture that's portable to Go.
5
+
6
+ Quick Start:
7
+ from ftl2 import automation
8
+
9
+ async with automation() as ftl:
10
+ await ftl.file(path="/tmp/test", state="touch")
11
+ await ftl.command(cmd="echo hello")
12
+ """
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ from ftl2.automation import automation, AutomationContext
17
+
18
+ __all__ = ["__version__", "automation", "AutomationContext"]
ftl2/arguments.py ADDED
@@ -0,0 +1,116 @@
1
+ """Argument merging and resolution for module execution.
2
+
3
+ This module provides argument handling logic that combines base module arguments
4
+ with host-specific overrides, resolving any variable references in the process.
5
+
6
+ Key Features:
7
+ - Merge base module arguments with host-specific overrides
8
+ - Resolve Ref objects against host data
9
+ - Precedence: host_args > dereferenced refs > literals
10
+ - Type-safe with dataclasses
11
+ """
12
+
13
+ from dataclasses import dataclass, field
14
+ from typing import Any
15
+
16
+ from ftl2.refs import Ref, deref
17
+ from ftl2.types import HostConfig
18
+
19
+
20
+ @dataclass
21
+ class ArgumentConfig:
22
+ """Configuration for module argument handling.
23
+
24
+ Attributes:
25
+ module_args: Base arguments to pass to all modules. Can contain Ref
26
+ objects for dynamic variable resolution.
27
+ host_args: Mapping of host names to host-specific argument overrides.
28
+ These have higher precedence than module_args.
29
+ """
30
+
31
+ module_args: dict[str, Any] = field(default_factory=dict)
32
+ host_args: dict[str, dict[str, Any]] = field(default_factory=dict)
33
+
34
+
35
+ def has_refs(args: dict[str, Any] | None) -> bool:
36
+ """Check if an argument dictionary contains any Ref objects.
37
+
38
+ Args:
39
+ args: Dictionary to check for Ref objects
40
+
41
+ Returns:
42
+ True if any value in the dictionary is a Ref object
43
+
44
+ Example:
45
+ >>> config = Ref(None, "config")
46
+ >>> has_refs({"src": "/tmp", "dest": "/var"})
47
+ False
48
+ >>> has_refs({"src": config.src_dir, "dest": "/var"})
49
+ True
50
+ """
51
+ if not args:
52
+ return False
53
+
54
+ return any(isinstance(value, Ref) for value in args.values())
55
+
56
+
57
+ def merge_arguments(
58
+ host: HostConfig,
59
+ module_args: dict[str, Any] | None,
60
+ host_args: dict[str, dict[str, Any]] | None,
61
+ ) -> dict[str, Any]:
62
+ """Merge module arguments with host-specific overrides.
63
+
64
+ This function implements the argument merging logic with proper precedence:
65
+ 1. host_args (highest precedence)
66
+ 2. dereferenced refs from module_args
67
+ 3. literal values from module_args
68
+
69
+ Args:
70
+ host: Host configuration to use for dereferencing
71
+ module_args: Base arguments for all hosts
72
+ host_args: Host-specific argument overrides
73
+
74
+ Returns:
75
+ Merged and resolved argument dictionary
76
+
77
+ Example:
78
+ >>> host = HostConfig(
79
+ ... name="web1",
80
+ ... ansible_host="192.168.1.100",
81
+ ... config={"src_dir": "/opt/app"}
82
+ ... )
83
+ >>> config = Ref(None, "config")
84
+ >>> module_args = {"src": config.src_dir, "mode": "0755"}
85
+ >>> host_args = {"web1": {"dest": "/var/app"}}
86
+ >>> merge_arguments(host, module_args, host_args)
87
+ {'src': '/opt/app', 'mode': '0755', 'dest': '/var/app'}
88
+ """
89
+ # Get host-specific overrides for this host
90
+ host_specific_args = {}
91
+ if host_args:
92
+ host_specific_args = host_args.get(host.name, {})
93
+
94
+ # Check if we need to do any merging
95
+ has_refs_in_args = has_refs(module_args)
96
+
97
+ # Fast path: no host-specific args and no refs
98
+ if not host_specific_args and not has_refs_in_args:
99
+ return module_args or {}
100
+
101
+ # Slow path: need to merge and/or resolve refs
102
+ merged_args = {}
103
+
104
+ # Start with module_args (if any)
105
+ if module_args:
106
+ merged_args = module_args.copy()
107
+
108
+ # Resolve any refs (refs have lower precedence than host-specific args)
109
+ if has_refs_in_args:
110
+ for arg_name, arg_value in module_args.items():
111
+ merged_args[arg_name] = deref(host.vars, arg_value)
112
+
113
+ # Apply host-specific args (higher precedence)
114
+ merged_args.update(host_specific_args)
115
+
116
+ return merged_args
@@ -0,0 +1,275 @@
1
+ """FTL2 Automation Context Manager.
2
+
3
+ Provides a clean, AI-friendly interface for automation scripts:
4
+
5
+ import asyncio
6
+ from ftl2.automation import automation
7
+
8
+ async def main():
9
+ async with automation() as ftl:
10
+ await ftl.file(path="/tmp/test", state="directory")
11
+ await ftl.copy(src="config.yml", dest="/etc/app/config.yml")
12
+ response = await ftl.uri(url="https://api.example.com/health")
13
+
14
+ asyncio.run(main())
15
+
16
+ The context manager provides:
17
+ - Clean ftl.module_name() syntax
18
+ - Automatic module discovery
19
+ - Check mode (dry-run) support
20
+ - Execution result tracking
21
+ - 250x faster than subprocess execution
22
+
23
+ This module is designed for AI-generated automation scripts where
24
+ readability and natural language patterns are important.
25
+ """
26
+
27
+ from contextlib import asynccontextmanager
28
+ from typing import Any, AsyncGenerator, Callable
29
+
30
+ from ftl2.automation.context import (
31
+ AutomationContext,
32
+ AutomationError,
33
+ OutputMode,
34
+ EventCallback,
35
+ )
36
+ from ftl2.automation.proxy import (
37
+ ModuleProxy,
38
+ NamespaceProxy,
39
+ HostScopedProxy,
40
+ HostScopedModuleProxy,
41
+ )
42
+
43
+ __all__ = [
44
+ "automation",
45
+ "AutomationContext",
46
+ "AutomationError",
47
+ "ModuleProxy",
48
+ "NamespaceProxy",
49
+ "HostScopedProxy",
50
+ "HostScopedModuleProxy",
51
+ "OutputMode",
52
+ ]
53
+
54
+
55
+ @asynccontextmanager
56
+ async def automation(
57
+ modules: list[str] | None = None,
58
+ inventory: str | None = None,
59
+ secrets: list[str] | None = None,
60
+ secret_bindings: dict[str, dict[str, str]] | None = None,
61
+ check_mode: bool = False,
62
+ verbose: bool = False,
63
+ quiet: bool = False,
64
+ on_event: EventCallback | None = None,
65
+ fail_fast: bool = False,
66
+ print_summary: bool = True,
67
+ print_errors: bool = True,
68
+ auto_install_deps: bool = False,
69
+ record_deps: bool = False,
70
+ deps_file: str = ".ftl2-deps.txt",
71
+ modules_file: str = ".ftl2-modules.txt",
72
+ gate_modules: "list[str] | str | None" = None,
73
+ gate_subsystem: bool = False,
74
+ state_file: str | None = ".ftl2-state.json",
75
+ record: str | None = None,
76
+ replay: str | None = None,
77
+ vault_secrets: dict[str, str] | None = None,
78
+ policy: str | None = None,
79
+ environment: str = "",
80
+ ) -> AsyncGenerator[AutomationContext, None]:
81
+ """Create an automation context for running FTL modules.
82
+
83
+ This is the main entry point for automation scripts. It provides
84
+ a clean interface where modules are accessed as attributes:
85
+
86
+ async with automation() as ftl:
87
+ await ftl.file(path="/tmp/test", state="touch")
88
+
89
+ Args:
90
+ modules: List of module names to enable. If None, all modules
91
+ are available. Use this to restrict which modules can
92
+ be called (e.g., for safety or documentation).
93
+ inventory: Path to inventory file, or None for localhost only.
94
+ Enables ftl.hosts access and ftl.run_on() for remote
95
+ execution.
96
+ secrets: List of environment variable names to load as secrets.
97
+ Access via ftl.secrets["NAME"]. Values are never logged.
98
+ secret_bindings: Automatic secret injection for modules. Maps module
99
+ patterns to {param: env_var} bindings. Secrets are injected
100
+ automatically so scripts never see actual values:
101
+ {"community.general.slack": {"token": "SLACK_TOKEN"}}
102
+ check_mode: Enable dry-run mode. Modules will report what they
103
+ would change without making actual changes.
104
+ verbose: Enable verbose output showing each module execution,
105
+ including timing information.
106
+ quiet: Suppress all output (overrides verbose). Useful for scripts
107
+ where you only want to check ftl.results programmatically.
108
+ on_event: Callback for structured events. Receives dict with keys:
109
+ event ("module_start" or "module_complete"), module, host,
110
+ timestamp, and event-specific data (success, changed, duration).
111
+ fail_fast: Stop execution on first error. Raises AutomationError
112
+ immediately when a module fails. Default is False (continue
113
+ and collect errors in ftl.errors).
114
+ print_summary: Print per-host summary on context exit. Default is True.
115
+ Shows counts of changed/ok/failed tasks per host.
116
+ print_errors: Print error summary on context exit. Default is True.
117
+ Set to False to handle errors manually via ftl.errors.
118
+ auto_install_deps: Automatically install missing Python dependencies
119
+ using uv when an Ansible module requires packages
120
+ that aren't installed. Default is False.
121
+ record_deps: Record module dependencies during execution and write
122
+ to deps_file on context exit. Use with auto_install_deps
123
+ for development to capture all needed packages.
124
+ deps_file: Path to write recorded dependencies. Default is
125
+ ".ftl2-deps.txt". Only used when record_deps=True.
126
+ modules_file: Path to write recorded module names. Default is
127
+ ".ftl2-modules.txt". Only used when record_deps=True.
128
+ gate_modules: Modules to bake into the gate for remote execution.
129
+ Accepts a list of module names, "auto" to read from
130
+ modules_file (or record on first run), or None for
131
+ per-task module transfer (default).
132
+ gate_subsystem: Register the gate as an SSH subsystem on remote
133
+ hosts. Requires root. Eliminates shell startup
134
+ overhead on subsequent connections. Default False.
135
+ state_file: Path to state file for persistent host/resource tracking.
136
+ When enabled, add_host() persists to state file immediately,
137
+ and hosts are loaded from state on context enter. Enables
138
+ crash recovery and idempotent provisioning. Default is
139
+ ".ftl2-state.json". Pass None to disable.
140
+ record: Path to JSON file for recording all actions as an audit
141
+ trail. Written on context exit with timestamps, durations,
142
+ parameters (excluding secrets), and results. Default is None.
143
+ replay: Path to a previous audit recording JSON file. When provided,
144
+ successful actions are skipped (returning cached output) and
145
+ execution resumes from the first unmatched or failed action.
146
+ Matching is positional. Use with record= to write a new audit
147
+ log including both replayed and newly executed actions.
148
+ vault_secrets: Mapping of secret names to HashiCorp Vault KV v2
149
+ references in "path#field" format. Secrets are read from Vault
150
+ at startup and accessible via ftl.secrets["NAME"]. Requires
151
+ VAULT_ADDR and VAULT_TOKEN environment variables. Example:
152
+ vault_secrets={"DB_PW": "myapp#db_password"}
153
+ Can also be referenced in secret_bindings for auto-injection.
154
+ policy: Path to a YAML policy file. When provided, every module
155
+ execution is checked against policy rules before running.
156
+ A matching deny rule raises PolicyDeniedError.
157
+ environment: Environment label for policy matching (e.g., "prod",
158
+ "staging"). Used by policy rules with environment conditions.
159
+
160
+ Yields:
161
+ AutomationContext with ftl.module_name() access to all modules
162
+
163
+ Raises:
164
+ AutomationError: If fail_fast=True and a module fails
165
+
166
+ Example:
167
+ # Basic usage (localhost)
168
+ async with automation() as ftl:
169
+ await ftl.file(path="/tmp/test", state="touch")
170
+ await ftl.command(cmd="echo hello")
171
+
172
+ # With inventory for remote execution
173
+ async with automation(inventory="hosts.yml") as ftl:
174
+ # Local execution
175
+ await ftl.file(path="/tmp/test", state="touch")
176
+
177
+ # Remote execution on hosts/groups
178
+ await ftl.run_on("webservers", "file", path="/var/www", state="directory")
179
+ await ftl.run_on(ftl.hosts["db01"], "command", cmd="pg_dump mydb")
180
+
181
+ # With secrets
182
+ async with automation(secrets=["AWS_ACCESS_KEY_ID", "API_TOKEN"]) as ftl:
183
+ key = ftl.secrets["AWS_ACCESS_KEY_ID"] # Get value
184
+ if "API_TOKEN" in ftl.secrets: # Check exists
185
+ token = ftl.secrets["API_TOKEN"]
186
+
187
+ # Restricted modules
188
+ async with automation(modules=["file", "copy"]) as ftl:
189
+ await ftl.file(path="/tmp/test", state="touch")
190
+ await ftl.command(cmd="echo") # Raises AttributeError
191
+
192
+ # Check mode (dry run)
193
+ async with automation(check_mode=True) as ftl:
194
+ await ftl.file(path="/tmp/test", state="absent")
195
+ # Reports what would be deleted without deleting
196
+
197
+ # Verbose output with timing
198
+ async with automation(verbose=True) as ftl:
199
+ await ftl.file(path="/tmp/test", state="touch")
200
+ # Prints: [file] ok (changed) (0.02s)
201
+
202
+ # Quiet mode for scripts
203
+ async with automation(quiet=True) as ftl:
204
+ await ftl.file(path="/tmp/test", state="touch")
205
+ # No output, check ftl.results for status
206
+
207
+ # Event callback for custom handling
208
+ events = []
209
+ async with automation(on_event=events.append) as ftl:
210
+ await ftl.file(path="/tmp/test", state="touch")
211
+ print(f"Collected {len(events)} events")
212
+
213
+ # Error handling - collect and inspect
214
+ async with automation() as ftl:
215
+ await ftl.file(path="/nonexistent/path", state="touch") # May fail
216
+ await ftl.file(path="/tmp/test", state="touch") # Still runs
217
+
218
+ if ftl.failed:
219
+ for error in ftl.errors:
220
+ print(f"Error in {error.module}: {error.error}")
221
+
222
+ # Error handling - fail fast
223
+ try:
224
+ async with automation(fail_fast=True) as ftl:
225
+ await ftl.file(path="/nonexistent/path", state="touch")
226
+ # Raises AutomationError, stops here
227
+ except AutomationError as e:
228
+ print(f"Failed: {e}")
229
+
230
+ # Secret bindings - inject secrets without script access
231
+ async with automation(
232
+ secret_bindings={
233
+ "community.general.slack": {"token": "SLACK_TOKEN"},
234
+ "amazon.aws.*": {"aws_access_key_id": "AWS_KEY"},
235
+ }
236
+ ) as ftl:
237
+ # Token injected automatically - script never sees it
238
+ await ftl.community.general.slack(channel="#deploy", msg="Done!")
239
+
240
+ Note:
241
+ Module execution is 250x faster than subprocess-based Ansible
242
+ because FTL modules run in-process as Python functions.
243
+ """
244
+ context = AutomationContext(
245
+ modules=modules,
246
+ inventory=inventory,
247
+ secrets=secrets,
248
+ secret_bindings=secret_bindings,
249
+ check_mode=check_mode,
250
+ verbose=verbose,
251
+ quiet=quiet,
252
+ on_event=on_event,
253
+ fail_fast=fail_fast,
254
+ print_summary=print_summary,
255
+ print_errors=print_errors,
256
+ auto_install_deps=auto_install_deps,
257
+ record_deps=record_deps,
258
+ deps_file=deps_file,
259
+ modules_file=modules_file,
260
+ gate_modules=gate_modules,
261
+ gate_subsystem=gate_subsystem,
262
+ state_file=state_file,
263
+ record=record,
264
+ replay=replay,
265
+ vault_secrets=vault_secrets,
266
+ policy=policy,
267
+ environment=environment,
268
+ )
269
+
270
+ try:
271
+ async with context:
272
+ yield context
273
+ finally:
274
+ # Any additional cleanup would go here
275
+ pass