1#[macro_use]
9extern crate version;
10#[macro_use]
11pub mod macros;
12
13pub mod builder;
14pub mod completion;
15pub mod error_formatter;
16pub mod git;
17pub mod init;
18pub mod integrations;
19pub mod logger;
20pub mod mcp;
21pub mod py_logger;
22pub mod python_bindings;
23pub mod task;
24pub mod utils;
25pub mod validation;
26
27use builder::{build_app, command_tree, tree_output};
28use error_formatter::PythonErrorFormatter;
29use integrations::uv::{UvIntegration, UvVirtualEnv};
30use task::ANGREAL_TASKS;
31
32use pyo3::types::{IntoPyDict, PyDict};
33use std::ops::Not;
34use std::path::{Path, PathBuf};
35use std::vec::Vec;
36
37use std::process::exit;
38
39use pyo3::{prelude::*, wrap_pymodule, IntoPyObjectExt};
40use std::collections::HashMap;
41use std::fs;
42
43use log::{debug, error, warn};
44
45use crate::integrations::git::Git;
46use crate::task::{generate_command_path_key, generate_path_key_from_parts};
47
48#[pyclass]
49struct PyGit {
50 inner: Git,
51}
52
53#[pymethods]
54impl PyGit {
55 #[new]
56 #[pyo3(signature = (working_dir=None))]
57 fn new(working_dir: Option<&str>) -> PyResult<Self> {
58 let git = Git::new(working_dir.map(Path::new))
59 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
60 Ok(Self { inner: git })
61 }
62
63 fn execute(&self, subcommand: &str, args: Vec<String>) -> PyResult<(i32, String, String)> {
64 let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
65 let output = self
66 .inner
67 .execute(subcommand, &arg_refs)
68 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
69 Ok((output.exit_code, output.stderr, output.stdout))
70 }
71
72 fn init(&self, bare: Option<bool>) -> PyResult<()> {
73 self.inner
74 .init(bare.unwrap_or(false))
75 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
76 }
77
78 fn add(&self, paths: Vec<String>) -> PyResult<()> {
79 let path_refs: Vec<&str> = paths.iter().map(|s| s.as_str()).collect();
80 self.inner
81 .add(&path_refs)
82 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
83 }
84
85 fn commit(&self, message: &str, all: Option<bool>) -> PyResult<()> {
86 self.inner
87 .commit(message, all.unwrap_or(false))
88 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
89 }
90
91 fn push(&self, remote: Option<&str>, branch: Option<&str>) -> PyResult<()> {
92 self.inner
93 .push(remote, branch)
94 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
95 }
96
97 fn pull(&self, remote: Option<&str>, branch: Option<&str>) -> PyResult<()> {
98 self.inner
99 .pull(remote, branch)
100 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
101 }
102
103 fn status(&self, short: Option<bool>) -> PyResult<String> {
104 self.inner
105 .status(short.unwrap_or(false))
106 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
107 }
108
109 fn branch(&self, name: Option<&str>, delete: Option<bool>) -> PyResult<String> {
110 self.inner
111 .branch(name, delete.unwrap_or(false))
112 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
113 }
114
115 fn checkout(&self, branch: &str, create: Option<bool>) -> PyResult<()> {
116 self.inner
117 .checkout(branch, create.unwrap_or(false))
118 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
119 }
120
121 fn tag(&self, name: &str, message: Option<&str>) -> PyResult<()> {
122 self.inner
123 .tag(name, message)
124 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
125 }
126
127 fn __call__(
128 &self,
129 command: &str,
130 args: Vec<String>,
131 kwargs: Option<&Bound<'_, pyo3::types::PyDict>>,
132 ) -> PyResult<(i32, String, String)> {
133 let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
134 let output = if let Some(dict) = kwargs {
135 let mut options_owned = HashMap::new();
137 for (key, value) in dict.iter() {
138 let key_str = key.extract::<String>()?;
139 let value_str = if value.is_truthy()? {
140 "".to_string() } else {
142 value.extract::<String>()?
143 };
144 options_owned.insert(key_str, value_str);
145 }
146 let options: HashMap<&str, &str> = options_owned
147 .iter()
148 .map(|(k, v)| (k.as_str(), v.as_str()))
149 .collect();
150 self.inner.execute_with_options(command, options, &arg_refs)
151 } else {
152 self.inner.execute(command, &arg_refs)
153 }
154 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
155
156 Ok((output.exit_code, output.stderr, output.stdout))
157 }
158}
159
160#[pyfunction]
161#[pyo3(signature = (remote, destination=None))]
162fn git_clone(remote: &str, destination: Option<&str>) -> PyResult<String> {
163 let dest = Git::clone(remote, destination.map(Path::new))
164 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
165 Ok(dest.display().to_string())
166}
167
168#[pyfunction]
169fn ensure_uv_installed() -> PyResult<()> {
170 UvIntegration::ensure_installed()
171 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
172}
173
174#[pyfunction]
175fn uv_version() -> PyResult<String> {
176 UvIntegration::version()
177 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
178}
179
180#[pyfunction]
181fn create_virtualenv(path: &str, python_version: Option<&str>) -> PyResult<()> {
182 UvVirtualEnv::create(Path::new(path), python_version)
183 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
184 Ok(())
185}
186
187#[pyfunction]
188fn install_packages(venv_path: &str, packages: Vec<String>) -> PyResult<()> {
189 let venv = UvVirtualEnv {
190 path: PathBuf::from(venv_path),
191 };
192 venv.install_packages(&packages)
193 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
194}
195
196#[pyfunction]
197fn install_requirements(venv_path: &str, requirements_file: &str) -> PyResult<()> {
198 let venv = UvVirtualEnv {
199 path: PathBuf::from(venv_path),
200 };
201 venv.install_requirements(Path::new(requirements_file))
202 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
203}
204
205#[pyfunction]
206fn discover_pythons() -> PyResult<Vec<(String, String)>> {
207 UvVirtualEnv::discover_pythons()
208 .map(|pythons| {
209 pythons
210 .into_iter()
211 .map(|(version, path)| (version, path.display().to_string()))
212 .collect()
213 })
214 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
215}
216
217#[pyfunction]
218fn install_python(version: &str) -> PyResult<String> {
219 UvVirtualEnv::install_python(version)
220 .map(|path| path.display().to_string())
221 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
222}
223
224#[pyfunction]
225fn get_venv_activation_info(venv_path: &str) -> PyResult<integrations::uv::ActivationInfo> {
226 let venv = UvVirtualEnv {
227 path: PathBuf::from(venv_path),
228 };
229 venv.get_activation_info()
230 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
231}
232
233#[pyfunction]
234fn register_entrypoint(name: &str) -> PyResult<()> {
235 use home::home_dir;
236 use serde_json;
237
238 let home = if let Some(home_from_env) = std::env::var_os("HOME") {
240 PathBuf::from(home_from_env)
241 } else if let Some(userprofile) = std::env::var_os("USERPROFILE") {
242 PathBuf::from(userprofile)
243 } else {
244 home_dir().ok_or_else(|| {
245 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Cannot find home directory")
246 })?
247 };
248
249 let local_bin = home.join(".local").join("bin");
251 fs::create_dir_all(&local_bin).map_err(|e| {
252 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
253 "Failed to create bin directory: {}",
254 e
255 ))
256 })?;
257
258 let data_dir = home.join(".angrealrc");
259 fs::create_dir_all(&data_dir).map_err(|e| {
260 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
261 "Failed to create data directory: {}",
262 e
263 ))
264 })?;
265
266 #[cfg(unix)]
268 let script_path = local_bin.join(name);
269 #[cfg(windows)]
270 let script_path = local_bin.join(format!("{}.bat", name));
271
272 if script_path.exists() {
274 return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
275 "Command '{}' already exists at {}",
276 name,
277 script_path.display()
278 )));
279 }
280
281 #[cfg(unix)]
283 {
284 let script_content = format!(
285 "#!/usr/bin/env python\n# ANGREAL_ALIAS: {}\n# Auto-generated by angreal.register_entrypoint\nimport sys\ntry:\n import angreal\n angreal.main()\nexcept ImportError:\n print(f\"Error: angreal not installed. Remove alias: rm {}\", file=sys.stderr)\n sys.exit(1)\n",
286 name,
287 script_path.display()
288 );
289
290 fs::write(&script_path, script_content).map_err(|e| {
291 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
292 "Failed to write script: {}",
293 e
294 ))
295 })?;
296
297 use std::os::unix::fs::PermissionsExt;
299 let mut perms = fs::metadata(&script_path)
300 .map_err(|e| {
301 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
302 "Failed to get permissions: {}",
303 e
304 ))
305 })?
306 .permissions();
307 perms.set_mode(0o755);
308 fs::set_permissions(&script_path, perms).map_err(|e| {
309 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
310 "Failed to set permissions: {}",
311 e
312 ))
313 })?;
314 }
315
316 #[cfg(windows)]
317 {
318 let script_content = format!(
319 "@echo off\nREM ANGREAL_ALIAS: {}\nREM Auto-generated by angreal.register_entrypoint\npython -m angreal %*\n",
320 name
321 );
322 fs::write(&script_path, script_content).map_err(|e| {
323 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
324 "Failed to write script: {}",
325 e
326 ))
327 })?;
328 }
329
330 let registry_path = home.join(".angrealrc").join("aliases.json");
332 let mut aliases: Vec<String> = if registry_path.exists() {
333 let content = fs::read_to_string(®istry_path).map_err(|e| {
334 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
335 "Failed to read registry: {}",
336 e
337 ))
338 })?;
339 serde_json::from_str(&content).unwrap_or_else(|_| Vec::new())
340 } else {
341 Vec::new()
342 };
343
344 if !aliases.contains(&name.to_string()) {
345 aliases.push(name.to_string());
346 let json = serde_json::to_string_pretty(&aliases).map_err(|e| {
347 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
348 "Failed to serialize registry: {}",
349 e
350 ))
351 })?;
352 fs::write(®istry_path, json).map_err(|e| {
353 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
354 "Failed to write registry: {}",
355 e
356 ))
357 })?;
358 }
359
360 println!("✅ Registered '{}' as angreal alias", name);
361 println!("Make sure ~/.local/bin is in your PATH");
362 Ok(())
363}
364
365#[pyfunction]
366fn list_entrypoints() -> PyResult<Vec<String>> {
367 use home::home_dir;
368
369 let home = if let Some(home_from_env) = std::env::var_os("HOME") {
371 PathBuf::from(home_from_env)
372 } else if let Some(userprofile) = std::env::var_os("USERPROFILE") {
373 PathBuf::from(userprofile)
374 } else {
375 home_dir().ok_or_else(|| {
376 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Cannot find home directory")
377 })?
378 };
379
380 let registry_path = home.join(".angrealrc").join("aliases.json");
381
382 if !registry_path.exists() {
383 return Ok(Vec::new());
384 }
385
386 let content = fs::read_to_string(®istry_path).map_err(|e| {
387 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to read registry: {}", e))
388 })?;
389
390 let aliases: Vec<String> = serde_json::from_str(&content).unwrap_or_else(|_| Vec::new());
391 Ok(aliases)
392}
393
394#[pyfunction]
395fn unregister_entrypoint(name: &str) -> PyResult<()> {
396 use home::home_dir;
397
398 let home = if let Some(home_from_env) = std::env::var_os("HOME") {
400 PathBuf::from(home_from_env)
401 } else if let Some(userprofile) = std::env::var_os("USERPROFILE") {
402 PathBuf::from(userprofile)
403 } else {
404 home_dir().ok_or_else(|| {
405 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Cannot find home directory")
406 })?
407 };
408
409 let local_bin = home.join(".local").join("bin");
411 #[cfg(unix)]
412 let script_path = local_bin.join(name);
413 #[cfg(windows)]
414 let script_path = local_bin.join(format!("{}.bat", name));
415
416 if script_path.exists() {
417 fs::remove_file(&script_path).map_err(|e| {
418 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
419 "Failed to remove script: {}",
420 e
421 ))
422 })?;
423 }
424
425 let registry_path = home.join(".angrealrc").join("aliases.json");
427
428 if registry_path.exists() {
429 let content = fs::read_to_string(®istry_path).map_err(|e| {
430 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
431 "Failed to read registry: {}",
432 e
433 ))
434 })?;
435
436 let mut aliases: Vec<String> =
437 serde_json::from_str(&content).unwrap_or_else(|_| Vec::new());
438 aliases.retain(|alias| alias != name);
439
440 let json = serde_json::to_string_pretty(&aliases).map_err(|e| {
441 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
442 "Failed to serialize registry: {}",
443 e
444 ))
445 })?;
446 fs::write(®istry_path, json).map_err(|e| {
447 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
448 "Failed to write registry: {}",
449 e
450 ))
451 })?;
452 }
453
454 println!("✅ Unregistered '{}' alias", name);
455 Ok(())
456}
457
458#[pyfunction]
459fn cleanup_entrypoints() -> PyResult<()> {
460 let aliases = list_entrypoints()?;
461
462 for alias in aliases {
463 if let Err(e) = unregister_entrypoint(&alias) {
464 eprintln!("Warning: Failed to unregister '{}': {}", alias, e);
465 }
466 }
467
468 println!("✅ Cleaned up all angreal aliases");
469 Ok(())
470}
471
472#[pyfunction]
474fn main() -> PyResult<()> {
475 let handle = logger::init_logger();
476 if std::env::var("ANGREAL_DEBUG").unwrap_or_default() == "true" {
477 logger::update_verbosity(&handle, 2);
478 warn!("Angreal application starting with debug level logging from environment");
479 }
480 debug!("Angreal application starting...");
481
482 let mut argvs: Vec<String> = std::env::args().collect();
485 argvs = argvs.split_off(2);
486
487 if let Err(e) = completion::auto_install_completion() {
489 warn!("Failed to auto-install shell completion: {}", e);
490 }
491
492 debug!("Checking if binary is up to date...");
493 match utils::check_up_to_date() {
494 Ok(()) => (),
495 Err(e) => warn!(
496 "An error occurred while checking if our binary is up to date. {}",
497 e
498 ),
499 };
500
501 let angreal_project_result = utils::is_angreal_project();
503 let in_angreal_project = angreal_project_result.is_ok();
504
505 if in_angreal_project {
506 debug!("Angreal project detected, loading found tasks.");
507 let angreal_path = angreal_project_result.expect("Expected angreal project path");
508 let angreal_tasks_to_load = utils::get_task_files(angreal_path);
510
511 let _angreal_tasks_to_load = match angreal_tasks_to_load {
513 Ok(tasks) => tasks,
514 Err(_) => {
515 error!("Exiting due to unrecoverable error.");
516 exit(1);
517 }
518 };
519
520 for task in _angreal_tasks_to_load.iter() {
522 if let Err(e) = utils::load_python(task.clone()) {
523 error!("Failed to load Python task: {}", e);
524 }
525 }
526 }
527
528 let app = build_app(in_angreal_project);
529 let mut app_copy = app.clone();
530 let sub_command = app.get_matches_from(&argvs);
531
532 let verbosity = sub_command.get_count("verbose");
534
535 if std::env::var("ANGREAL_DEBUG").is_err() {
537 logger::update_verbosity(&handle, verbosity);
538 debug!("Log verbosity set to level: {}", verbosity);
539 }
540
541 match sub_command.subcommand() {
542 Some(("init", _sub_matches)) => init::init(
543 _sub_matches.value_of("template").unwrap(),
544 _sub_matches.is_present("force"),
545 _sub_matches.is_present("defaults").not(),
546 if _sub_matches.is_present("values_file") {
547 Some(_sub_matches.value_of("values_file").unwrap())
548 } else {
549 None
550 },
551 ),
552 Some(("_complete", _sub_matches)) => {
553 let args: Vec<String> = _sub_matches
555 .values_of("args")
556 .unwrap_or_default()
557 .map(|s| s.to_string())
558 .collect();
559
560 match completion::generate_completions(&args) {
561 Ok(completions) => {
562 for completion in completions {
563 println!("{}", completion);
564 }
565 }
566 Err(e) => {
567 debug!("Completion generation failed: {}", e);
568 }
569 }
570 return Ok(());
571 }
572 Some(("_completion", _sub_matches)) => {
573 let shell = _sub_matches.value_of("shell").unwrap_or("bash");
575 match shell {
576 "bash" => println!("{}", completion::bash::generate_completion_script()),
577 "zsh" => println!("{}", completion::zsh::generate_completion_script()),
578 _ => {
579 error!("Unsupported shell for completion: {}", shell);
580 exit(1);
581 }
582 }
583 return Ok(());
584 }
585 Some(("alias", sub_matches)) => {
586 match sub_matches.subcommand() {
588 Some(("create", create_matches)) => {
589 let name = create_matches.value_of("name").unwrap();
590 Python::attach(|_py| {
591 if let Err(e) = register_entrypoint(name) {
592 error!("Failed to create alias: {}", e);
593 exit(1);
594 }
595 });
596 }
597 Some(("remove", remove_matches)) => {
598 let name = remove_matches.value_of("name").unwrap();
599 Python::attach(|_py| {
600 if let Err(e) = unregister_entrypoint(name) {
601 error!("Failed to remove alias: {}", e);
602 exit(1);
603 }
604 });
605 }
606 Some(("list", _)) => {
607 Python::attach(|_py| match list_entrypoints() {
608 Ok(aliases) => {
609 if aliases.is_empty() {
610 println!("No aliases registered.");
611 } else {
612 println!("Registered aliases:");
613 for alias in aliases {
614 println!(" {}", alias);
615 }
616 }
617 }
618 Err(e) => {
619 error!("Failed to list aliases: {}", e);
620 exit(1);
621 }
622 });
623 }
624 _ => {
625 error!("Invalid alias subcommand. Use 'create', 'remove', or 'list'.");
626 exit(1);
627 }
628 }
629 return Ok(());
630 }
631 Some(("completion", sub_matches)) => {
632 match sub_matches.subcommand() {
634 Some(("install", install_matches)) => {
635 let shell = install_matches.value_of("shell");
636 match crate::completion::force_install_completion(shell) {
637 Ok(()) => {}
638 Err(e) => {
639 error!("Failed to install completion: {}", e);
640 exit(1);
641 }
642 }
643 }
644 Some(("uninstall", uninstall_matches)) => {
645 let shell = uninstall_matches.value_of("shell");
646 match crate::completion::uninstall_completion(shell) {
647 Ok(()) => {}
648 Err(e) => {
649 error!("Failed to uninstall completion: {}", e);
650 exit(1);
651 }
652 }
653 }
654 Some(("status", _)) => match crate::completion::show_completion_status() {
655 Ok(()) => {}
656 Err(e) => {
657 error!("Failed to show completion status: {}", e);
658 exit(1);
659 }
660 },
661 _ => {
662 error!(
663 "Invalid completion subcommand. Use 'install', 'uninstall', or 'status'."
664 );
665 exit(1);
666 }
667 }
668 return Ok(());
669 }
670 Some(("tree", sub_matches)) => {
671 if !in_angreal_project {
672 error!("This doesn't appear to be an angreal project.");
673 exit(1);
674 }
675
676 let mut root = command_tree::CommandNode::new_group("angreal".to_string(), None);
678 for (_, cmd) in ANGREAL_TASKS.lock().unwrap().iter() {
679 root.add_command(cmd.clone());
680 }
681
682 let long = sub_matches.get_flag("long");
683 tree_output::print_tree(&root, long);
684
685 return Ok(());
686 }
687 Some(("mcp", _)) => {
688 if !in_angreal_project {
689 error!("This doesn't appear to be an angreal project.");
690 exit(1);
691 }
692
693 mcp::serve();
694 return Ok(());
695 }
696 Some((task, sub_m)) => {
697 if !in_angreal_project {
698 error!("This doesn't appear to be an angreal project.");
699 exit(1)
700 }
701
702 let mut command_groups: Vec<String> = Vec::new();
703 command_groups.push(task.to_string());
704
705 let mut next = sub_m.subcommand();
708 let mut arg_matches = sub_m.clone();
709 while next.is_some() {
710 let cmd = next.unwrap();
711 command_groups.push(cmd.0.to_string());
712 next = cmd.1.subcommand();
713 arg_matches = cmd.1.clone();
714 }
715
716 let task = command_groups.pop().unwrap();
717
718 let command_path = generate_path_key_from_parts(&command_groups, &task);
720 let tasks_registry = ANGREAL_TASKS.lock().unwrap();
721
722 debug!("Looking up command with path: {}", command_path);
723 let (registry_key, command) = match tasks_registry
726 .iter()
727 .find(|(_, cmd)| generate_command_path_key(cmd) == command_path)
728 {
729 None => {
730 error!("Command '{}' not found.", task);
731 app_copy.print_help().unwrap_or(());
732 exit(1)
733 }
734 Some((key, found_command)) => (key.clone(), found_command),
735 };
736
737 debug!(
738 "Executing command: {} (registry key: {})",
739 task, registry_key
740 );
741
742 let args = builder::select_args(®istry_key);
743 Python::attach(|py| {
744 debug!("Starting Python execution for command: {}", task);
745 let mut kwargs: Vec<(&str, Py<PyAny>)> = Vec::new();
746
747 for arg in args.into_iter() {
748 let n = Box::leak(Box::new(arg.name));
749 if arg.is_flag.unwrap() {
753 let v = arg_matches.get_flag(&n.clone());
754 kwargs.push((
755 n.as_str(),
756 v.into_bound_py_any(py)
757 .expect("Failed to convert to Python object")
758 .unbind(),
759 ));
760 } else {
761 let v = arg_matches.value_of(n.clone());
762 match v {
763 None => {
764 kwargs.push((
767 n.as_str(),
768 v.into_bound_py_any(py)
769 .expect("Failed to convert to Python object")
770 .unbind(),
771 ));
772 }
773 Some(v) => match arg.python_type.unwrap().as_str() {
774 "str" => kwargs.push((
775 n.as_str(),
776 v.into_bound_py_any(py)
777 .expect("Failed to convert to Python object")
778 .unbind(),
779 )),
780 "int" => kwargs.push((
781 n.as_str(),
782 v.parse::<i32>()
783 .unwrap()
784 .into_bound_py_any(py)
785 .expect("Failed to convert to Python object")
786 .unbind(),
787 )),
788 "float" => kwargs.push((
789 n.as_str(),
790 v.parse::<f32>()
791 .unwrap()
792 .into_bound_py_any(py)
793 .expect("Failed to convert to Python object")
794 .unbind(),
795 )),
796 _ => kwargs.push((
797 n.as_str(),
798 v.into_bound_py_any(py)
799 .expect("Failed to convert to Python object")
800 .unbind(),
801 )),
802 },
803 }
804 }
805 }
806
807 let kwargs_dict = match kwargs.into_py_dict(py) {
808 Ok(dict) => dict,
809 Err(err) => {
810 error!("Failed to convert kwargs to dict");
811 let formatter = PythonErrorFormatter::new(err);
812 println!("{}", formatter);
813 exit(1);
814 }
815 };
816 let r_value = command.func.call(py, (), Some(&kwargs_dict));
817
818 match r_value {
819 Ok(r_value) => {
820 if let Ok(val) = r_value.extract::<bool>(py) {
823 if !val {
824 exit(1);
825 }
826 } else if let Ok(code) = r_value.extract::<i32>(py) {
827 if code != 0 {
828 exit(code);
829 }
830 }
831 debug!("Successfully executed Python command: {}", task);
833 }
834 Err(err) => {
835 let is_sys_exit = err
837 .value(py)
838 .get_type()
839 .name()
840 .map(|n| n == "SystemExit")
841 .unwrap_or(false);
842 if is_sys_exit {
843 let code = err
844 .value(py)
845 .getattr("code")
846 .and_then(|c| c.extract::<i32>())
847 .unwrap_or(1);
848 exit(code);
849 }
850 error!("Failed to execute Python command: {}", task);
851 let formatter = PythonErrorFormatter::new(err);
852 println!("{}", formatter);
853 exit(56);
854 }
855 }
856 });
857 }
858 _ => {
859 println!("process for current context")
860 }
861 }
862
863 debug!("Angreal application completed successfully.");
864 Ok(())
865}
866
867pub fn initialize_python_tasks() -> Result<(), Box<dyn std::error::Error>> {
870 use pyo3::types::PyDict;
871
872 debug!("Initializing Python bindings for angreal tasks");
873
874 Python::attach(|py| -> PyResult<()> {
876 let sys = PyModule::import(py, "sys")?;
878 let modules_attr = sys.getattr("modules")?;
879 let modules = modules_attr.cast::<PyDict>()?;
880
881 if !modules.contains("angreal")? {
883 debug!("Registering angreal module in Python sys.modules");
884
885 let angreal_module = PyModule::new(py, "angreal")?;
887
888 angreal_module.add("__version__", env!("CARGO_PKG_VERSION"))?;
890
891 py_logger::register();
893
894 task::register(py, &angreal_module)?;
896 utils::register(py, &angreal_module)?;
897 python_bindings::decorators::register_decorators(py, &angreal_module)?;
898
899 angreal_module
901 .add_wrapped(wrap_pymodule!(python_bindings::integrations::integrations))?;
902
903 modules.set_item(
905 "angreal.integrations",
906 angreal_module.getattr("integrations")?,
907 )?;
908 modules.set_item(
909 "angreal.integrations.docker",
910 angreal_module
911 .getattr("integrations")?
912 .getattr("docker_integration")?,
913 )?;
914 modules.set_item(
915 "angreal.integrations.git",
916 angreal_module
917 .getattr("integrations")?
918 .getattr("git_integration")?,
919 )?;
920 modules.set_item(
921 "angreal.integrations.venv",
922 angreal_module.getattr("integrations")?.getattr("venv")?,
923 )?;
924 modules.set_item(
925 "angreal.integrations.flox",
926 angreal_module.getattr("integrations")?.getattr("flox")?,
927 )?;
928
929 modules.set_item("angreal", angreal_module)?;
931
932 debug!("Successfully registered angreal module in Python");
933 } else {
934 debug!("Angreal module already available in sys.modules");
935 }
936
937 Ok(())
938 })?;
939
940 let angreal_path =
942 utils::is_angreal_project().map_err(|e| format!("Not in angreal project: {}", e))?;
943
944 debug!("Found angreal project at: {}", angreal_path.display());
945
946 let task_files = utils::get_task_files(angreal_path)
948 .map_err(|e| format!("Failed to get task files: {}", e))?;
949
950 debug!("Found {} task files to load", task_files.len());
951
952 for task_file in task_files.iter() {
954 debug!("Loading Python task file: {}", task_file.display());
955
956 match utils::load_python(task_file.clone()) {
957 Ok(_) => debug!("Successfully loaded task file: {}", task_file.display()),
958 Err(e) => {
959 warn!("Failed to load task file {}: {}", task_file.display(), e);
960 }
962 }
963 }
964
965 let task_count = ANGREAL_TASKS.lock().unwrap().len();
966 debug!("Successfully initialized {} angreal tasks", task_count);
967
968 Ok(())
969}
970
971#[pymodule]
972fn angreal(m: &Bound<'_, PyModule>) -> PyResult<()> {
973 m.add("__version__", env!("CARGO_PKG_VERSION"))?;
974
975 py_logger::register();
976 m.add_function(wrap_pyfunction!(main, m)?)?;
977 task::register(m.py(), m)?;
978 utils::register(m.py(), m)?;
979
980 python_bindings::decorators::register_decorators(m.py(), m)?;
982
983 m.add_function(wrap_pyfunction!(ensure_uv_installed, m)?)?;
985 m.add_function(wrap_pyfunction!(uv_version, m)?)?;
986 m.add_function(wrap_pyfunction!(create_virtualenv, m)?)?;
987 m.add_function(wrap_pyfunction!(install_packages, m)?)?;
988 m.add_function(wrap_pyfunction!(install_requirements, m)?)?;
989 m.add_function(wrap_pyfunction!(discover_pythons, m)?)?;
990 m.add_function(wrap_pyfunction!(install_python, m)?)?;
991 m.add_function(wrap_pyfunction!(get_venv_activation_info, m)?)?;
992 m.add_class::<integrations::uv::ActivationInfo>()?;
993
994 m.add_function(wrap_pyfunction!(register_entrypoint, m)?)?;
996 m.add_function(wrap_pyfunction!(list_entrypoints, m)?)?;
997 m.add_function(wrap_pyfunction!(unregister_entrypoint, m)?)?;
998 m.add_function(wrap_pyfunction!(cleanup_entrypoints, m)?)?;
999
1000 let integrations_module = PyModule::new(m.py(), "integrations")?;
1001 python_bindings::integrations::integrations(m.py(), &integrations_module)?;
1002 m.add_submodule(&integrations_module)?;
1003
1004 let sys = PyModule::import(m.py(), "sys")?;
1005 let modules_attr = sys.getattr("modules")?;
1006 let sys_modules = modules_attr.cast::<PyDict>()?;
1007 sys_modules.set_item("angreal.integrations", m.getattr("integrations")?)?;
1008 sys_modules.set_item(
1009 "angreal.integrations.docker",
1010 m.getattr("integrations")?.getattr("docker_integration")?,
1011 )?;
1012
1013 sys_modules.set_item(
1014 "angreal.integrations.docker.image",
1015 m.getattr("integrations")?
1016 .getattr("docker_integration")?
1017 .getattr("image")?,
1018 )?;
1019 sys_modules.set_item(
1020 "angreal.integrations.docker.container",
1021 m.getattr("integrations")?
1022 .getattr("docker_integration")?
1023 .getattr("container")?,
1024 )?;
1025 sys_modules.set_item(
1026 "angreal.integrations.docker.network",
1027 m.getattr("integrations")?
1028 .getattr("docker_integration")?
1029 .getattr("network")?,
1030 )?;
1031 sys_modules.set_item(
1032 "angreal.integrations.docker.volume",
1033 m.getattr("integrations")?
1034 .getattr("docker_integration")?
1035 .getattr("volume")?,
1036 )?;
1037
1038 sys_modules.set_item(
1039 "angreal.integrations.git",
1040 m.getattr("integrations")?.getattr("git_integration")?,
1041 )?;
1042
1043 sys_modules.set_item(
1044 "angreal.integrations.venv",
1045 m.getattr("integrations")?.getattr("venv")?,
1046 )?;
1047
1048 sys_modules.set_item(
1049 "angreal.integrations.flox",
1050 m.getattr("integrations")?.getattr("flox")?,
1051 )?;
1052
1053 Ok(())
1054}
1055
1056#[pymodule]
1057fn _integrations(m: &Bound<'_, PyModule>) -> PyResult<()> {
1058 let docker_module = pyo3::wrap_pymodule!(docker)(m.py());
1059 m.add_submodule(docker_module.bind(m.py()))?;
1060 let git = pyo3::wrap_pymodule!(git_module)(m.py());
1061 m.add_submodule(git.bind(m.py()))?;
1062 Ok(())
1063}
1064
1065#[pymodule]
1066fn docker(m: &Bound<'_, PyModule>) -> PyResult<()> {
1067 m.add_class::<docker_pyo3::Pyo3Docker>()?;
1068
1069 let image_module = PyModule::new(m.py(), "image")?;
1070 docker_pyo3::image::image(m.py(), &image_module)?;
1071 m.add_submodule(&image_module)?;
1072
1073 let container_module = PyModule::new(m.py(), "container")?;
1074 docker_pyo3::container::container(m.py(), &container_module)?;
1075 m.add_submodule(&container_module)?;
1076
1077 let network_module = PyModule::new(m.py(), "network")?;
1078 docker_pyo3::network::network(m.py(), &network_module)?;
1079 m.add_submodule(&network_module)?;
1080
1081 let volume_module = PyModule::new(m.py(), "volume")?;
1082 docker_pyo3::volume::volume(m.py(), &volume_module)?;
1083 m.add_submodule(&volume_module)?;
1084 Ok(())
1085}
1086
1087#[pymodule]
1088fn git_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1089 m.add_class::<PyGit>()?;
1090 m.add_function(wrap_pyfunction!(git_clone, m)?)?;
1091 Ok(())
1092}