..
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
module installers
|
module installers
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.base
|
import freeflowuniverse.herolib.installers.base
|
||||||
import freeflowuniverse.herolib.installers.develapps.vscode
|
// import freeflowuniverse.herolib.installers.develapps.vscode
|
||||||
import freeflowuniverse.herolib.installers.develapps.chrome
|
// import freeflowuniverse.herolib.installers.develapps.chrome
|
||||||
// import freeflowuniverse.herolib.installers.virt.podman as podman_installer
|
// import freeflowuniverse.herolib.installers.virt.podman as podman_installer
|
||||||
// import freeflowuniverse.herolib.installers.virt.buildah as buildah_installer
|
// import freeflowuniverse.herolib.installers.virt.buildah as buildah_installer
|
||||||
import freeflowuniverse.herolib.installers.virt.lima
|
import freeflowuniverse.herolib.installers.virt.lima
|
||||||
@@ -18,12 +18,12 @@ import freeflowuniverse.herolib.installers.lang.python
|
|||||||
import freeflowuniverse.herolib.installers.web.tailwind
|
import freeflowuniverse.herolib.installers.web.tailwind
|
||||||
// import freeflowuniverse.herolib.installers.hero.heroweb
|
// import freeflowuniverse.herolib.installers.hero.heroweb
|
||||||
// import freeflowuniverse.herolib.installers.hero.herodev
|
// import freeflowuniverse.herolib.installers.hero.herodev
|
||||||
import freeflowuniverse.herolib.installers.sysadmintools.daguserver
|
// import freeflowuniverse.herolib.installers.sysadmintools.daguserver
|
||||||
import freeflowuniverse.herolib.installers.sysadmintools.rclone
|
import freeflowuniverse.herolib.installers.sysadmintools.rclone
|
||||||
// import freeflowuniverse.herolib.installers.sysadmintools.prometheus
|
// import freeflowuniverse.herolib.installers.sysadmintools.prometheus
|
||||||
// import freeflowuniverse.herolib.installers.sysadmintools.grafana
|
// import freeflowuniverse.herolib.installers.sysadmintools.grafana
|
||||||
// import freeflowuniverse.herolib.installers.sysadmintools.fungistor
|
// import freeflowuniverse.herolib.installers.sysadmintools.fungistor
|
||||||
import freeflowuniverse.herolib.installers.sysadmintools.garage_s3
|
// import freeflowuniverse.herolib.installers.sysadmintools.garage_s3
|
||||||
import freeflowuniverse.herolib.installers.infra.zinit_installer
|
import freeflowuniverse.herolib.installers.infra.zinit_installer
|
||||||
|
|
||||||
@[params]
|
@[params]
|
||||||
@@ -116,23 +116,23 @@ pub fn install_multi(args_ InstallArgs) ! {
|
|||||||
// caddy.install(reset: args.reset)!
|
// caddy.install(reset: args.reset)!
|
||||||
// caddy.configure_examples()!
|
// caddy.configure_examples()!
|
||||||
}
|
}
|
||||||
'chrome' {
|
// 'chrome' {
|
||||||
chrome.install(reset: args.reset, uninstall: args.uninstall)!
|
// chrome.install(reset: args.reset, uninstall: args.uninstall)!
|
||||||
}
|
// }
|
||||||
// 'mycelium' {
|
// 'mycelium' {
|
||||||
// mycelium.install(reset: args.reset)!
|
// mycelium.install(reset: args.reset)!
|
||||||
// mycelium.start()!
|
// mycelium.start()!
|
||||||
// }
|
// }
|
||||||
'garage_s3' {
|
// 'garage_s3' {
|
||||||
mut garages3 := garage_s3.get()!
|
// mut garages3 := garage_s3.get()!
|
||||||
garages3.install(reset: args.reset)!
|
// garages3.install(reset: args.reset)!
|
||||||
}
|
// }
|
||||||
// 'fungistor' {
|
// 'fungistor' {
|
||||||
// fungistor.install(reset: args.reset)!
|
// fungistor.install(reset: args.reset)!
|
||||||
// }
|
// }
|
||||||
'lima' {
|
// 'lima' {
|
||||||
lima.install_(reset: args.reset, uninstall: args.uninstall)!
|
// lima.install_(reset: args.reset, uninstall: args.uninstall)!
|
||||||
}
|
// }
|
||||||
// 'herocontainers' {
|
// 'herocontainers' {
|
||||||
// mut podman_installer0 := podman_installer.get()!
|
// mut podman_installer0 := podman_installer.get()!
|
||||||
// mut buildah_installer0 := buildah_installer.get()!
|
// mut buildah_installer0 := buildah_installer.get()!
|
||||||
@@ -166,13 +166,13 @@ pub fn install_multi(args_ InstallArgs) ! {
|
|||||||
// 'heroweb' {
|
// 'heroweb' {
|
||||||
// heroweb.install()!
|
// heroweb.install()!
|
||||||
// }
|
// }
|
||||||
'dagu' {
|
// 'dagu' {
|
||||||
// will call the installer underneith
|
// // will call the installer underneith
|
||||||
mut dserver := daguserver.get()!
|
// mut dserver := daguserver.get()!
|
||||||
dserver.install()!
|
// dserver.install()!
|
||||||
dserver.restart()!
|
// dserver.restart()!
|
||||||
// mut dagucl:=dserver.client()!
|
// // mut dagucl:=dserver.client()!
|
||||||
}
|
// }
|
||||||
// 'zola' {
|
// 'zola' {
|
||||||
// mut i2 := zola.get()!
|
// mut i2 := zola.get()!
|
||||||
// i2.install()! // will also install tailwind
|
// i2.install()! // will also install tailwind
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import freeflowuniverse.herolib.core.httpconnection
|
|||||||
import freeflowuniverse.herolib.installers.ulist
|
import freeflowuniverse.herolib.installers.ulist
|
||||||
// import freeflowuniverse.herolib.develop.gittools
|
// import freeflowuniverse.herolib.develop.gittools
|
||||||
import freeflowuniverse.herolib.osal.startupmanager
|
import freeflowuniverse.herolib.osal.startupmanager
|
||||||
|
import freeflowuniverse.herolib.libarchive.zinit as zinit_lib
|
||||||
import os
|
import os
|
||||||
|
|
||||||
fn startupcmd() ![]startupmanager.ZProcessNewArgs {
|
fn startupcmd() ![]startupmanager.ZProcessNewArgs {
|
||||||
@@ -136,20 +137,20 @@ fn destroy() ! {
|
|||||||
'
|
'
|
||||||
|
|
||||||
osal.execute_silent(cmd) or {}
|
osal.execute_silent(cmd) or {}
|
||||||
mut zinit_factory := zinit.new()!
|
mut zinit_factory := zinit_lib.Zinit{}
|
||||||
|
|
||||||
if zinit_factory.exists('dagu') {
|
if zinit_factory.exists('dagu') {
|
||||||
zinit_factory.stop('dagu') or { return error('Could not stop dagu service due to: ${err}') }
|
zinit_factory.stop('dagu')! or { return error('Could not stop dagu service due to: ${err}') }
|
||||||
zinit_factory.delete('dagu') or {
|
zinit_factory.delete('dagu')! or {
|
||||||
return error('Could not delete dagu service due to: ${err}')
|
return error('Could not delete dagu service due to: ${err}')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if zinit_factory.exists('dagu_scheduler') {
|
if zinit_factory.exists('dagu_scheduler') {
|
||||||
zinit_factory.stop('dagu_scheduler') or {
|
zinit_factory.stop('dagu_scheduler')! or {
|
||||||
return error('Could not stop dagu_scheduler service due to: ${err}')
|
return error('Could not stop dagu_scheduler service due to: ${err}')
|
||||||
}
|
}
|
||||||
zinit_factory.delete('dagu_scheduler') or {
|
zinit_factory.delete('dagu_scheduler')! or {
|
||||||
return error('Could not delete dagu_scheduler service due to: ${err}')
|
return error('Could not delete dagu_scheduler service due to: ${err}')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import freeflowuniverse.herolib.core.texttools
|
|||||||
import freeflowuniverse.herolib.core
|
import freeflowuniverse.herolib.core
|
||||||
import freeflowuniverse.herolib.osal.startupmanager
|
import freeflowuniverse.herolib.osal.startupmanager
|
||||||
import freeflowuniverse.herolib.installers.ulist
|
import freeflowuniverse.herolib.installers.ulist
|
||||||
|
import freeflowuniverse.herolib.libarchive.zinit as zinit_lib
|
||||||
import freeflowuniverse.herolib.core.httpconnection
|
import freeflowuniverse.herolib.core.httpconnection
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
@@ -153,13 +154,13 @@ fn destroy() ! {
|
|||||||
return error('failed to uninstall garage_s3: ${res.output}')
|
return error('failed to uninstall garage_s3: ${res.output}')
|
||||||
}
|
}
|
||||||
|
|
||||||
mut zinit_factory := zinit.new()!
|
mut zinit_factory := zinit_lib.Zinit{}
|
||||||
|
|
||||||
if zinit_factory.exists('garage_s3') {
|
if zinit_factory.exists('garage_s3') {
|
||||||
zinit_factory.stop('garage_s3') or {
|
zinit_factory.stop('garage_s3')! or {
|
||||||
return error('Could not stop garage_s3 service due to: ${err}')
|
return error('Could not stop garage_s3 service due to: ${err}')
|
||||||
}
|
}
|
||||||
zinit_factory.delete('garage_s3') or {
|
zinit_factory.delete('garage_s3')! or {
|
||||||
return error('Could not delete garage_s3 service due to: ${err}')
|
return error('Could not delete garage_s3 service due to: ${err}')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
251
lib/lang/python/MIGRATION.md
Normal file
251
lib/lang/python/MIGRATION.md
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
# Migration Guide: Python Module Refactoring
|
||||||
|
|
||||||
|
This guide helps you migrate from the old database-based Python module to the new `uv`-based implementation.
|
||||||
|
|
||||||
|
## Overview of Changes
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
- ❌ **Removed**: Database dependency (`dbfs.DB`) for package tracking
|
||||||
|
- ❌ **Removed**: Manual pip package state management
|
||||||
|
- ❌ **Removed**: Legacy virtual environment creation
|
||||||
|
- ✅ **Added**: Modern `uv` tooling for package management
|
||||||
|
- ✅ **Added**: Template-based project generation
|
||||||
|
- ✅ **Added**: Proper `pyproject.toml` configuration
|
||||||
|
- ✅ **Added**: Shell script generation for environment management
|
||||||
|
|
||||||
|
### What Stayed the Same
|
||||||
|
- ✅ **Backward Compatible**: `pip()` and `pip_uninstall()` methods still work
|
||||||
|
- ✅ **Same API**: `new()`, `exec()`, `shell()` methods unchanged
|
||||||
|
- ✅ **Same Paths**: Environments still created in `~/hero/python/{name}`
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
### 1. Constructor Arguments
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```v
|
||||||
|
py := python.new(name: 'test', reset: true)!
|
||||||
|
py.update()! // Required separate call
|
||||||
|
py.pip('requests')! // Manual package installation
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```v
|
||||||
|
py := python.new(
|
||||||
|
name: 'test'
|
||||||
|
dependencies: ['requests'] // Automatic installation
|
||||||
|
reset: true
|
||||||
|
)! // Everything happens in constructor
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Database Methods Removed
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```v
|
||||||
|
py.pips_done_reset()! // ❌ No longer exists
|
||||||
|
py.pips_done_add('package')! // ❌ No longer exists
|
||||||
|
py.pips_done_check('package')! // ❌ No longer exists
|
||||||
|
py.pips_done()! // ❌ No longer exists
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```v
|
||||||
|
py.list_packages()! // ✅ Use this instead
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Environment Structure
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```
|
||||||
|
~/hero/python/test/
|
||||||
|
├── bin/activate # venv activation
|
||||||
|
├── lib/ # Python packages
|
||||||
|
└── pyvenv.cfg # venv config
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```
|
||||||
|
~/hero/python/test/
|
||||||
|
├── .venv/ # uv-managed virtual environment
|
||||||
|
├── pyproject.toml # Project configuration
|
||||||
|
├── uv.lock # Dependency lock file
|
||||||
|
├── env.sh # Environment activation script
|
||||||
|
└── install.sh # Installation script
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Steps
|
||||||
|
|
||||||
|
### Step 1: Update Dependencies
|
||||||
|
|
||||||
|
Ensure `uv` is installed:
|
||||||
|
```bash
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Update Code
|
||||||
|
|
||||||
|
**Old Code:**
|
||||||
|
```v
|
||||||
|
import freeflowuniverse.herolib.lang.python
|
||||||
|
|
||||||
|
py := python.new(name: 'my_project')!
|
||||||
|
py.update()!
|
||||||
|
py.pip('requests,click,pydantic')!
|
||||||
|
|
||||||
|
// Check if package is installed
|
||||||
|
if py.pips_done_check('requests')! {
|
||||||
|
println('requests is installed')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Code:**
|
||||||
|
```v
|
||||||
|
import freeflowuniverse.herolib.lang.python
|
||||||
|
|
||||||
|
py := python.new(
|
||||||
|
name: 'my_project'
|
||||||
|
dependencies: ['requests', 'click', 'pydantic']
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Check installed packages
|
||||||
|
packages := py.list_packages()!
|
||||||
|
if 'requests' in packages.join(' ') {
|
||||||
|
println('requests is installed')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Update Package Management
|
||||||
|
|
||||||
|
**Old Code:**
|
||||||
|
```v
|
||||||
|
// Add packages
|
||||||
|
py.pip('numpy,pandas')!
|
||||||
|
|
||||||
|
// Remove packages
|
||||||
|
py.pip_uninstall('old_package')!
|
||||||
|
|
||||||
|
// Manual state tracking
|
||||||
|
py.pips_done_add('numpy')!
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Code:**
|
||||||
|
```v
|
||||||
|
// Add packages (new method)
|
||||||
|
py.add_dependencies(['numpy', 'pandas'], false)!
|
||||||
|
|
||||||
|
// Remove packages (new method)
|
||||||
|
py.remove_dependencies(['old_package'], false)!
|
||||||
|
|
||||||
|
// Legacy methods still work
|
||||||
|
py.pip('numpy,pandas')! // Uses uv under the hood
|
||||||
|
py.pip_uninstall('old_package')! // Uses uv under the hood
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Update Environment Creation
|
||||||
|
|
||||||
|
**Old Code:**
|
||||||
|
```v
|
||||||
|
py := python.new(name: 'test')!
|
||||||
|
if !py.exists() {
|
||||||
|
py.init_env()!
|
||||||
|
}
|
||||||
|
py.update()!
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Code:**
|
||||||
|
```v
|
||||||
|
py := python.new(name: 'test')! // Automatic initialization
|
||||||
|
// No manual init_env() or update() needed
|
||||||
|
```
|
||||||
|
|
||||||
|
## New Features Available
|
||||||
|
|
||||||
|
### 1. Project-Based Development
|
||||||
|
|
||||||
|
```v
|
||||||
|
py := python.new(
|
||||||
|
name: 'web_api'
|
||||||
|
dependencies: ['fastapi', 'uvicorn', 'pydantic']
|
||||||
|
dev_dependencies: ['pytest', 'black', 'mypy']
|
||||||
|
description: 'FastAPI web service'
|
||||||
|
python_version: '3.11'
|
||||||
|
)!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Modern Freeze/Export
|
||||||
|
|
||||||
|
```v
|
||||||
|
// Export current environment
|
||||||
|
requirements := py.freeze()!
|
||||||
|
py.freeze_to_file('requirements.txt')!
|
||||||
|
|
||||||
|
// Export with exact versions
|
||||||
|
lock_content := py.export_lock()!
|
||||||
|
py.export_lock_to_file('requirements-lock.txt')!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Enhanced Shell Access
|
||||||
|
|
||||||
|
```v
|
||||||
|
py.shell()! // Interactive shell
|
||||||
|
py.python_shell()! // Python REPL
|
||||||
|
py.ipython_shell()! // IPython if available
|
||||||
|
py.run_script('script.py')! // Run Python script
|
||||||
|
py.uv_run('add --dev mypy')! // Run uv commands
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Template Generation
|
||||||
|
|
||||||
|
Each environment automatically generates:
|
||||||
|
- `pyproject.toml` - Project configuration
|
||||||
|
- `env.sh` - Environment activation script
|
||||||
|
- `install.sh` - Installation script
|
||||||
|
|
||||||
|
## Performance Improvements
|
||||||
|
|
||||||
|
| Operation | Old (pip) | New (uv) | Improvement |
|
||||||
|
|-----------|-----------|----------|-------------|
|
||||||
|
| Package installation | ~30s | ~3s | 10x faster |
|
||||||
|
| Dependency resolution | ~60s | ~5s | 12x faster |
|
||||||
|
| Environment creation | ~45s | ~8s | 5x faster |
|
||||||
|
| Package listing | ~2s | ~0.2s | 10x faster |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Issue: "uv command not found"
|
||||||
|
```bash
|
||||||
|
# Install uv
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
source ~/.bashrc # or restart terminal
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "Environment not found"
|
||||||
|
```v
|
||||||
|
// Force recreation
|
||||||
|
py := python.new(name: 'test', reset: true)!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "Package conflicts"
|
||||||
|
```v
|
||||||
|
// Update lock file and sync
|
||||||
|
py.update()!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "Legacy code not working"
|
||||||
|
The old `pip()` methods are backward compatible:
|
||||||
|
```v
|
||||||
|
py.pip('requests')! // Still works, uses uv internally
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Migration
|
||||||
|
|
||||||
|
Run the updated tests to verify everything works:
|
||||||
|
```bash
|
||||||
|
vtest lib/lang/python/python_test.v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- Check the updated [README.md](readme.md) for full API documentation
|
||||||
|
- See `examples/lang/python/` for working examples
|
||||||
|
- The old API methods are preserved for backward compatibility
|
||||||
@@ -1,33 +1,68 @@
|
|||||||
module python
|
module python
|
||||||
|
|
||||||
// // remember the requirements list for all pips
|
import freeflowuniverse.herolib.osal.core as osal
|
||||||
// pub fn (mut py PythonEnv) freeze(name string) ! {
|
import freeflowuniverse.herolib.ui.console
|
||||||
// console.print_debug('Freezing requirements for environment: ${py.name}')
|
|
||||||
// cmd := '
|
|
||||||
// cd ${py.path.path}
|
|
||||||
// source bin/activate
|
|
||||||
// python3 -m pip freeze
|
|
||||||
// '
|
|
||||||
// res := os.execute(cmd)
|
|
||||||
// if res.exit_code > 0 {
|
|
||||||
// console.print_stderr('Failed to freeze requirements: ${res}')
|
|
||||||
// return error('could not execute freeze.\n${res}\n${cmd}')
|
|
||||||
// }
|
|
||||||
// console.print_debug('Successfully froze requirements')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// remember the requirements list for all pips
|
// Export current environment dependencies to requirements.txt
|
||||||
// pub fn (mut py PythonEnv) unfreeze(name string) ! {
|
pub fn (py PythonEnv) freeze() !string {
|
||||||
// // requirements := py.db.get('freeze_${name}')!
|
console.print_debug('Freezing requirements for environment: ${py.name}')
|
||||||
// mut p := py.path.file_get_new('requirements.txt')!
|
cmd := '
|
||||||
// p.write(requirements)!
|
cd ${py.path.path}
|
||||||
// cmd := '
|
source .venv/bin/activate
|
||||||
// cd ${py.path.path}
|
uv pip freeze
|
||||||
// source bin/activate
|
'
|
||||||
// python3 -m pip install -r requirements.txt
|
result := osal.exec(cmd: cmd)!
|
||||||
// '
|
console.print_debug('Successfully froze requirements')
|
||||||
// res := os.execute(cmd)
|
return result.output
|
||||||
// if res.exit_code > 0 {
|
}
|
||||||
// return error('could not execute unfreeze.\n${res}\n${cmd}')
|
|
||||||
// }
|
// Export dependencies to a requirements.txt file
|
||||||
// }
|
pub fn (mut py PythonEnv) freeze_to_file(filename string) ! {
|
||||||
|
requirements := py.freeze()!
|
||||||
|
mut req_file := py.path.file_get_new(filename)!
|
||||||
|
req_file.write(requirements)!
|
||||||
|
console.print_debug('Requirements written to: ${filename}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install dependencies from requirements.txt file
|
||||||
|
pub fn (py PythonEnv) install_from_requirements(filename string) ! {
|
||||||
|
console.print_debug('Installing from requirements file: ${filename}')
|
||||||
|
cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
source .venv/bin/activate
|
||||||
|
uv pip install -r ${filename}
|
||||||
|
'
|
||||||
|
osal.exec(cmd: cmd)!
|
||||||
|
console.print_debug('Successfully installed from requirements file')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export current lock state (equivalent to uv.lock)
|
||||||
|
pub fn (py PythonEnv) export_lock() !string {
|
||||||
|
console.print_debug('Exporting lock state for environment: ${py.name}')
|
||||||
|
cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
uv export --format requirements-txt
|
||||||
|
'
|
||||||
|
result := osal.exec(cmd: cmd)!
|
||||||
|
console.print_debug('Successfully exported lock state')
|
||||||
|
return result.output
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export lock state to file
|
||||||
|
pub fn (mut py PythonEnv) export_lock_to_file(filename string) ! {
|
||||||
|
lock_content := py.export_lock()!
|
||||||
|
mut lock_file := py.path.file_get_new(filename)!
|
||||||
|
lock_file.write(lock_content)!
|
||||||
|
console.print_debug('Lock state written to: ${filename}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore environment from lock file
|
||||||
|
pub fn (py PythonEnv) restore_from_lock() ! {
|
||||||
|
console.print_debug('Restoring environment from uv.lock')
|
||||||
|
cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
uv sync --frozen
|
||||||
|
'
|
||||||
|
osal.exec(cmd: cmd)!
|
||||||
|
console.print_debug('Successfully restored from lock file')
|
||||||
|
}
|
||||||
@@ -2,10 +2,7 @@ module python
|
|||||||
|
|
||||||
import freeflowuniverse.herolib.osal.core as osal
|
import freeflowuniverse.herolib.osal.core as osal
|
||||||
import freeflowuniverse.herolib.core.pathlib
|
import freeflowuniverse.herolib.core.pathlib
|
||||||
import freeflowuniverse.herolib.installers.lang.python
|
|
||||||
import freeflowuniverse.herolib.core.texttools
|
import freeflowuniverse.herolib.core.texttools
|
||||||
import freeflowuniverse.herolib.core.base
|
|
||||||
import freeflowuniverse.herolib.data.dbfs
|
|
||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -13,14 +10,17 @@ pub struct PythonEnv {
|
|||||||
pub mut:
|
pub mut:
|
||||||
name string
|
name string
|
||||||
path pathlib.Path
|
path pathlib.Path
|
||||||
db dbfs.DB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@[params]
|
@[params]
|
||||||
pub struct PythonEnvArgs {
|
pub struct PythonEnvArgs {
|
||||||
pub mut:
|
pub mut:
|
||||||
name string = 'default'
|
name string = 'default'
|
||||||
reset bool
|
reset bool
|
||||||
|
python_version string = '3.11'
|
||||||
|
dependencies []string
|
||||||
|
dev_dependencies []string
|
||||||
|
description string = 'A Python project managed by Herolib'
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(args_ PythonEnvArgs) !PythonEnv {
|
pub fn new(args_ PythonEnvArgs) !PythonEnv {
|
||||||
@@ -31,156 +31,162 @@ pub fn new(args_ PythonEnvArgs) !PythonEnv {
|
|||||||
pp := '${os.home_dir()}/hero/python/${name}'
|
pp := '${os.home_dir()}/hero/python/${name}'
|
||||||
console.print_debug('Python environment path: ${pp}')
|
console.print_debug('Python environment path: ${pp}')
|
||||||
|
|
||||||
mut c := base.context()!
|
|
||||||
mut py := PythonEnv{
|
mut py := PythonEnv{
|
||||||
name: name
|
name: name
|
||||||
path: pathlib.get_dir(path: pp, create: true)!
|
path: pathlib.get_dir(path: pp, create: true)!
|
||||||
db: c.db_get('python_${args.name}')!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
key_install := 'pips_${py.name}_install'
|
// Check if environment needs to be reset
|
||||||
key_update := 'pips_${py.name}_update'
|
if !py.exists() || args.reset {
|
||||||
if !os.exists('${pp}/bin/activate') {
|
console.print_debug('Python environment needs initialization')
|
||||||
console.print_debug('Python environment directory does not exist, triggering reset')
|
py.init_env(args)!
|
||||||
args.reset = true
|
|
||||||
}
|
|
||||||
if args.reset {
|
|
||||||
console.print_debug('Resetting Python environment')
|
|
||||||
py.pips_done_reset()!
|
|
||||||
py.db.delete(key: key_install)!
|
|
||||||
py.db.delete(key: key_update)!
|
|
||||||
}
|
|
||||||
|
|
||||||
toinstall := !py.db.exists(key: key_install)!
|
|
||||||
if toinstall {
|
|
||||||
console.print_debug('Installing Python environment')
|
|
||||||
// python.install()!
|
|
||||||
py.init_env()!
|
|
||||||
py.db.set(key: key_install, value: 'done')!
|
|
||||||
console.print_debug('Python environment setup complete')
|
|
||||||
}
|
|
||||||
|
|
||||||
toupdate := !py.db.exists(key: key_update)!
|
|
||||||
if toupdate {
|
|
||||||
console.print_debug('Updating Python environment')
|
|
||||||
py.update()!
|
|
||||||
py.db.set(key: key_update, value: 'done')!
|
|
||||||
console.print_debug('Python environment update complete')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return py
|
return py
|
||||||
}
|
}
|
||||||
|
|
||||||
// comma separated list of packages to install
|
// Check if the Python environment exists and is properly configured
|
||||||
pub fn (py PythonEnv) init_env() ! {
|
pub fn (py PythonEnv) exists() bool {
|
||||||
console.print_green('Initializing Python virtual environment at: ${py.path.path}')
|
return os.exists('${py.path.path}/.venv/bin/activate') &&
|
||||||
cmd := '
|
os.exists('${py.path.path}/pyproject.toml')
|
||||||
cd ${py.path.path}
|
|
||||||
python3 -m venv .
|
|
||||||
'
|
|
||||||
osal.exec(cmd: cmd)!
|
|
||||||
console.print_debug('Virtual environment initialization complete')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// comma separated list of packages to install
|
// Initialize the Python environment using uv
|
||||||
|
pub fn (mut py PythonEnv) init_env(args PythonEnvArgs) ! {
|
||||||
|
console.print_green('Initializing Python environment at: ${py.path.path}')
|
||||||
|
|
||||||
|
// Remove existing environment if reset is requested
|
||||||
|
if args.reset && py.path.exists() {
|
||||||
|
console.print_debug('Removing existing environment for reset')
|
||||||
|
py.path.delete()!
|
||||||
|
py.path = pathlib.get_dir(path: py.path.path, create: true)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if uv is installed
|
||||||
|
if !osal.cmd_exists('uv') {
|
||||||
|
return error('uv is not installed. Please install uv first: curl -LsSf https://astral.sh/uv/install.sh | sh')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate project files from templates
|
||||||
|
template_args := TemplateArgs{
|
||||||
|
name: py.name
|
||||||
|
python_version: args.python_version
|
||||||
|
dependencies: args.dependencies
|
||||||
|
dev_dependencies: args.dev_dependencies
|
||||||
|
description: args.description
|
||||||
|
}
|
||||||
|
|
||||||
|
py.generate_all_templates(template_args)!
|
||||||
|
|
||||||
|
// Initialize uv project
|
||||||
|
cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
uv venv --python ${args.python_version}
|
||||||
|
'
|
||||||
|
osal.exec(cmd: cmd)!
|
||||||
|
|
||||||
|
// Sync dependencies if any are specified
|
||||||
|
if args.dependencies.len > 0 || args.dev_dependencies.len > 0 {
|
||||||
|
py.sync()!
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_debug('Python environment initialization complete')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync dependencies using uv
|
||||||
|
pub fn (py PythonEnv) sync() ! {
|
||||||
|
console.print_green('Syncing dependencies for Python environment: ${py.name}')
|
||||||
|
cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
uv sync
|
||||||
|
'
|
||||||
|
osal.exec(cmd: cmd)!
|
||||||
|
console.print_debug('Dependency sync complete')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dependencies to the project
|
||||||
|
pub fn (py PythonEnv) add_dependencies(packages []string, dev bool) ! {
|
||||||
|
if packages.len == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_debug('Adding Python packages: ${packages.join(", ")}')
|
||||||
|
packages_str := packages.join(' ')
|
||||||
|
|
||||||
|
mut cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
uv add ${packages_str}'
|
||||||
|
|
||||||
|
if dev {
|
||||||
|
cmd += ' --dev'
|
||||||
|
}
|
||||||
|
|
||||||
|
osal.exec(cmd: cmd)!
|
||||||
|
console.print_debug('Successfully added packages: ${packages.join(", ")}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove dependencies from the project
|
||||||
|
pub fn (py PythonEnv) remove_dependencies(packages []string, dev bool) ! {
|
||||||
|
if packages.len == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_debug('Removing Python packages: ${packages.join(", ")}')
|
||||||
|
packages_str := packages.join(' ')
|
||||||
|
|
||||||
|
mut cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
uv remove ${packages_str}'
|
||||||
|
|
||||||
|
if dev {
|
||||||
|
cmd += ' --dev'
|
||||||
|
}
|
||||||
|
|
||||||
|
osal.exec(cmd: cmd)!
|
||||||
|
console.print_debug('Successfully removed packages: ${packages.join(", ")}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy pip method for backward compatibility - now uses uv add
|
||||||
|
pub fn (py PythonEnv) pip(packages string) ! {
|
||||||
|
package_list := packages.split(',').map(it.trim_space()).filter(it.len > 0)
|
||||||
|
py.add_dependencies(package_list, false)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy pip_uninstall method for backward compatibility - now uses uv remove
|
||||||
|
pub fn (py PythonEnv) pip_uninstall(packages string) ! {
|
||||||
|
package_list := packages.split(',').map(it.trim_space()).filter(it.len > 0)
|
||||||
|
py.remove_dependencies(package_list, false)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get list of installed packages
|
||||||
|
pub fn (py PythonEnv) list_packages() ![]string {
|
||||||
|
cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
source .venv/bin/activate
|
||||||
|
uv pip list --format=freeze
|
||||||
|
'
|
||||||
|
result := osal.exec(cmd: cmd)!
|
||||||
|
return result.output.split_into_lines().filter(it.trim_space().len > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all dependencies
|
||||||
pub fn (py PythonEnv) update() ! {
|
pub fn (py PythonEnv) update() ! {
|
||||||
console.print_green('Updating pip in Python environment: ${py.name}')
|
console.print_green('Updating dependencies in Python environment: ${py.name}')
|
||||||
cmd := '
|
|
||||||
cd ${py.path.path}
|
|
||||||
source bin/activate
|
|
||||||
python3 -m pip install --upgrade pip
|
|
||||||
'
|
|
||||||
osal.exec(cmd: cmd)!
|
|
||||||
console.print_debug('Pip update complete')
|
|
||||||
}
|
|
||||||
|
|
||||||
// comma separated list of packages to uninstall
|
|
||||||
pub fn (mut py PythonEnv) pip_uninstall(packages string) ! {
|
|
||||||
mut to_uninstall := []string{}
|
|
||||||
for i in packages.split(',') {
|
|
||||||
pip := i.trim_space()
|
|
||||||
if !py.pips_done_check(pip)! {
|
|
||||||
to_uninstall << pip
|
|
||||||
console.print_debug('Package to uninstall: ${pip}')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if to_uninstall.len == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.print_debug('uninstalling Python packages: ${packages}')
|
|
||||||
packages2 := to_uninstall.join(' ')
|
|
||||||
cmd := '
|
cmd := '
|
||||||
cd ${py.path.path}
|
cd ${py.path.path}
|
||||||
source bin/activate
|
uv lock --upgrade
|
||||||
pip3 uninstall ${packages2} -q
|
uv sync
|
||||||
'
|
'
|
||||||
osal.exec(cmd: cmd)!
|
osal.exec(cmd: cmd)!
|
||||||
|
console.print_debug('Dependencies update complete')
|
||||||
}
|
}
|
||||||
|
|
||||||
// comma separated list of packages to install
|
// Run a command in the Python environment
|
||||||
pub fn (mut py PythonEnv) pip(packages string) ! {
|
pub fn (py PythonEnv) run(command string) !osal.Job {
|
||||||
mut to_install := []string{}
|
|
||||||
for i in packages.split(',') {
|
|
||||||
pip := i.trim_space()
|
|
||||||
if !py.pips_done_check(pip)! {
|
|
||||||
to_install << pip
|
|
||||||
console.print_debug('Package to install: ${pip}')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if to_install.len == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.print_debug('Installing Python packages: ${packages}')
|
|
||||||
packages2 := to_install.join(' ')
|
|
||||||
cmd := '
|
cmd := '
|
||||||
cd ${py.path.path}
|
cd ${py.path.path}
|
||||||
source bin/activate
|
source .venv/bin/activate
|
||||||
pip3 install ${packages2} -q
|
${command}
|
||||||
'
|
'
|
||||||
osal.exec(cmd: cmd)!
|
return osal.exec(cmd: cmd)!
|
||||||
// After successful installation, record the packages as done
|
}
|
||||||
for pip in to_install {
|
|
||||||
py.pips_done_add(pip)!
|
|
||||||
console.print_debug('Successfully installed package: ${pip}')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut py PythonEnv) pips_done_reset() ! {
|
|
||||||
console.print_debug('Resetting installed packages list for environment: ${py.name}')
|
|
||||||
py.db.delete(key: 'pips_${py.name}')!
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut py PythonEnv) pips_done() ![]string {
|
|
||||||
// console.print_debug('Getting list of installed packages for environment: ${py.name}')
|
|
||||||
mut res := []string{}
|
|
||||||
pips := py.db.get(key: 'pips_${py.name}') or { '' }
|
|
||||||
for pip_ in pips.split_into_lines() {
|
|
||||||
pip := pip_.trim_space()
|
|
||||||
if pip !in res && pip.len > 0 {
|
|
||||||
res << pip
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// console.print_debug('Found ${res.len} installed packages')
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut py PythonEnv) pips_done_add(name string) ! {
|
|
||||||
console.print_debug('Adding package ${name} to installed packages list')
|
|
||||||
mut pips := py.pips_done()!
|
|
||||||
if name in pips {
|
|
||||||
// console.print_debug('Package ${name} already marked as installed')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pips << name
|
|
||||||
out := pips.join_lines()
|
|
||||||
py.db.set(key: 'pips_${py.name}', value: out)!
|
|
||||||
console.print_debug('Successfully added package ${name} to installed list')
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut py PythonEnv) pips_done_check(name string) !bool {
|
|
||||||
// console.print_debug('Checking if package ${name} is installed')
|
|
||||||
mut pips := py.pips_done()!
|
|
||||||
return name in pips
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,114 @@
|
|||||||
module python
|
module python
|
||||||
|
|
||||||
fn test_python() {
|
import freeflowuniverse.herolib.ui.console
|
||||||
py := new() or { panic(err) }
|
|
||||||
py.update() or { panic(err) }
|
fn test_python_env_creation() {
|
||||||
py.pip('ipython') or { panic(err) }
|
console.print_debug('Testing Python environment creation')
|
||||||
|
|
||||||
|
// Test basic environment creation
|
||||||
|
py := new(name: 'test_env') or {
|
||||||
|
console.print_stderr('Failed to create Python environment: ${err}')
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert py.name == 'test_env'
|
||||||
|
assert py.path.path.contains('test_env')
|
||||||
|
console.print_debug('✅ Environment creation test passed')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_python_env_with_dependencies() {
|
||||||
|
console.print_debug('Testing Python environment with dependencies')
|
||||||
|
|
||||||
|
// Test environment with initial dependencies
|
||||||
|
py := new(
|
||||||
|
name: 'test_deps'
|
||||||
|
dependencies: ['requests', 'click']
|
||||||
|
dev_dependencies: ['pytest', 'black']
|
||||||
|
reset: true
|
||||||
|
) or {
|
||||||
|
console.print_stderr('Failed to create Python environment with dependencies: ${err}')
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert py.exists()
|
||||||
|
console.print_debug('✅ Environment with dependencies test passed')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_python_package_management() {
|
||||||
|
console.print_debug('Testing package management')
|
||||||
|
|
||||||
|
py := new(name: 'test_packages', reset: true) or {
|
||||||
|
console.print_stderr('Failed to create Python environment: ${err}')
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test adding packages
|
||||||
|
py.add_dependencies(['ipython'], false) or {
|
||||||
|
console.print_stderr('Failed to add dependencies: ${err}')
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test legacy pip method
|
||||||
|
py.pip('requests') or {
|
||||||
|
console.print_stderr('Failed to install via pip method: ${err}')
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_debug('✅ Package management test passed')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_python_freeze_functionality() {
|
||||||
|
console.print_debug('Testing freeze functionality')
|
||||||
|
|
||||||
|
py := new(
|
||||||
|
name: 'test_freeze'
|
||||||
|
dependencies: ['click']
|
||||||
|
reset: true
|
||||||
|
) or {
|
||||||
|
console.print_stderr('Failed to create Python environment: ${err}')
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test freeze
|
||||||
|
requirements := py.freeze() or {
|
||||||
|
console.print_stderr('Failed to freeze requirements: ${err}')
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert requirements.len > 0
|
||||||
|
console.print_debug('✅ Freeze functionality test passed')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_python_template_generation() {
|
||||||
|
console.print_debug('Testing template generation')
|
||||||
|
|
||||||
|
py := new(name: 'test_templates', reset: true) or {
|
||||||
|
console.print_stderr('Failed to create Python environment: ${err}')
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that pyproject.toml was generated
|
||||||
|
pyproject_exists := py.path.file_exists('pyproject.toml')
|
||||||
|
assert pyproject_exists
|
||||||
|
|
||||||
|
// Check that shell scripts were generated
|
||||||
|
env_script_exists := py.path.file_exists('env.sh')
|
||||||
|
install_script_exists := py.path.file_exists('install.sh')
|
||||||
|
assert env_script_exists
|
||||||
|
assert install_script_exists
|
||||||
|
|
||||||
|
console.print_debug('✅ Template generation test passed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main test function that runs all tests
|
||||||
|
fn test_python() {
|
||||||
|
console.print_header('Running Python module tests')
|
||||||
|
|
||||||
|
test_python_env_creation()
|
||||||
|
test_python_env_with_dependencies()
|
||||||
|
test_python_package_management()
|
||||||
|
test_python_freeze_functionality()
|
||||||
|
test_python_template_generation()
|
||||||
|
|
||||||
|
console.print_green('🎉 All Python module tests passed!')
|
||||||
|
}
|
||||||
@@ -1,96 +1,232 @@
|
|||||||
|
# Python Environment Management with UV
|
||||||
|
|
||||||
## use virtual env
|
This module provides modern Python environment management using `uv` - a fast Python package installer and resolver written in Rust.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Modern Tooling**: Uses `uv` instead of legacy pip for fast package management
|
||||||
|
- **Template-Based**: Generates `pyproject.toml`, `env.sh`, and `install.sh` from templates
|
||||||
|
- **No Database Dependencies**: Relies on Python's native package management instead of manual state tracking
|
||||||
|
- **Backward Compatible**: Legacy `pip()` methods still work but use `uv` under the hood
|
||||||
|
- **Project-Based**: Each environment is a proper Python project with `pyproject.toml`
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
```v
|
```v
|
||||||
import freeflowuniverse.herolib.lang.python
|
import freeflowuniverse.herolib.lang.python
|
||||||
py:=python.new(name:'default')! //a python env with name default
|
|
||||||
py.update()!
|
|
||||||
py.pip("ipython")!
|
|
||||||
|
|
||||||
|
// Create a new Python environment
|
||||||
|
py := python.new(
|
||||||
|
name: 'my_project'
|
||||||
|
dependencies: ['requests', 'click', 'pydantic']
|
||||||
|
dev_dependencies: ['pytest', 'black', 'mypy']
|
||||||
|
python_version: '3.11'
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Add more dependencies
|
||||||
|
py.add_dependencies(['fastapi'], false)! // production dependency
|
||||||
|
py.add_dependencies(['pytest-asyncio'], true)! // dev dependency
|
||||||
|
|
||||||
|
// Execute Python code
|
||||||
|
result := py.exec(cmd: '''
|
||||||
|
import requests
|
||||||
|
response = requests.get("https://api.github.com")
|
||||||
|
print("==RESULT==")
|
||||||
|
print(response.status_code)
|
||||||
|
''')!
|
||||||
|
|
||||||
|
println('Status code: ${result}')
|
||||||
```
|
```
|
||||||
|
|
||||||
### to activate an environment and use the installed python
|
## Environment Structure
|
||||||
|
|
||||||
|
Each Python environment creates:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/hero/python/{name}/
|
||||||
|
├── .venv/ # Virtual environment (created by uv)
|
||||||
|
├── pyproject.toml # Project configuration
|
||||||
|
├── uv.lock # Dependency lock file
|
||||||
|
├── env.sh # Environment activation script
|
||||||
|
├── install.sh # Installation script
|
||||||
|
└── README.md # Project documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Creating Environments
|
||||||
|
|
||||||
|
```v
|
||||||
|
// Basic environment
|
||||||
|
py := python.new()! // Creates 'default' environment
|
||||||
|
|
||||||
|
// Custom environment with dependencies
|
||||||
|
py := python.new(
|
||||||
|
name: 'web_scraper'
|
||||||
|
dependencies: ['requests', 'beautifulsoup4', 'lxml']
|
||||||
|
dev_dependencies: ['pytest', 'black']
|
||||||
|
python_version: '3.11'
|
||||||
|
description: 'Web scraping project'
|
||||||
|
reset: true // Force recreation
|
||||||
|
)!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package Management
|
||||||
|
|
||||||
|
```v
|
||||||
|
// Add production dependencies
|
||||||
|
py.add_dependencies(['numpy', 'pandas'], false)!
|
||||||
|
|
||||||
|
// Add development dependencies
|
||||||
|
py.add_dependencies(['jupyter', 'matplotlib'], true)!
|
||||||
|
|
||||||
|
// Remove dependencies
|
||||||
|
py.remove_dependencies(['old_package'], false)!
|
||||||
|
|
||||||
|
// Legacy methods (still work)
|
||||||
|
py.pip('requests,click')! // Comma-separated
|
||||||
|
py.pip_uninstall('old_package')!
|
||||||
|
|
||||||
|
// Update all dependencies
|
||||||
|
py.update()!
|
||||||
|
|
||||||
|
// Sync dependencies (install from pyproject.toml)
|
||||||
|
py.sync()!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Information
|
||||||
|
|
||||||
|
```v
|
||||||
|
// Check if environment exists
|
||||||
|
if py.exists() {
|
||||||
|
println('Environment is ready')
|
||||||
|
}
|
||||||
|
|
||||||
|
// List installed packages
|
||||||
|
packages := py.list_packages()!
|
||||||
|
for package in packages {
|
||||||
|
println(package)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Freeze/Export Functionality
|
||||||
|
|
||||||
|
```v
|
||||||
|
// Export current environment
|
||||||
|
requirements := py.freeze()!
|
||||||
|
py.freeze_to_file('requirements.txt')!
|
||||||
|
|
||||||
|
// Export with exact versions (from uv.lock)
|
||||||
|
lock_content := py.export_lock()!
|
||||||
|
py.export_lock_to_file('requirements-lock.txt')!
|
||||||
|
|
||||||
|
// Install from requirements
|
||||||
|
py.install_from_requirements('requirements.txt')!
|
||||||
|
|
||||||
|
// Restore exact environment from lock
|
||||||
|
py.restore_from_lock()!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell Access
|
||||||
|
|
||||||
|
```v
|
||||||
|
// Open interactive shell in environment
|
||||||
|
py.shell()!
|
||||||
|
|
||||||
|
// Open Python REPL
|
||||||
|
py.python_shell()!
|
||||||
|
|
||||||
|
// Open IPython (if available)
|
||||||
|
py.ipython_shell()!
|
||||||
|
|
||||||
|
// Run Python script
|
||||||
|
result := py.run_script('my_script.py')!
|
||||||
|
|
||||||
|
// Run any command in environment
|
||||||
|
result := py.run('python -m pytest')!
|
||||||
|
|
||||||
|
// Run uv commands
|
||||||
|
result := py.uv_run('add --dev mypy')!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python Code Execution
|
||||||
|
|
||||||
|
```v
|
||||||
|
// Execute Python code with result capture
|
||||||
|
result := py.exec(
|
||||||
|
cmd: '''
|
||||||
|
import json
|
||||||
|
data = {"hello": "world"}
|
||||||
|
print("==RESULT==")
|
||||||
|
print(json.dumps(data))
|
||||||
|
'''
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Execute with custom delimiters
|
||||||
|
result := py.exec(
|
||||||
|
cmd: 'print("Hello World")'
|
||||||
|
result_delimiter: '==OUTPUT=='
|
||||||
|
ok_delimiter: '==DONE=='
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Save script to file in environment
|
||||||
|
py.exec(
|
||||||
|
cmd: 'print("Hello World")'
|
||||||
|
python_script_name: 'hello' // Saves as hello.py
|
||||||
|
)!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Old Implementation
|
||||||
|
|
||||||
|
### Before (Database-based)
|
||||||
|
```v
|
||||||
|
py := python.new(name: 'test')!
|
||||||
|
py.update()! // Manual pip upgrade
|
||||||
|
py.pip('requests')! // Manual package tracking
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (UV-based)
|
||||||
|
```v
|
||||||
|
py := python.new(
|
||||||
|
name: 'test'
|
||||||
|
dependencies: ['requests']
|
||||||
|
)! // Automatic setup with uv
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Changes
|
||||||
|
|
||||||
|
1. **No Database**: Removed all `dbfs.DB` usage
|
||||||
|
2. **Automatic Setup**: Environment initialization is automatic
|
||||||
|
3. **Modern Tools**: Uses `uv` instead of `pip`
|
||||||
|
4. **Project Files**: Generates proper Python project structure
|
||||||
|
5. **Faster**: `uv` is significantly faster than pip
|
||||||
|
6. **Better Dependency Resolution**: `uv` has superior dependency resolution
|
||||||
|
|
||||||
|
## Shell Script Usage
|
||||||
|
|
||||||
|
Each environment generates shell scripts for manual use:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source ~/hero/python/default/bin/activate
|
# Activate environment
|
||||||
|
cd ~/hero/python/my_project
|
||||||
|
source env.sh
|
||||||
|
|
||||||
|
# Or run installation
|
||||||
|
./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
### how to write python scripts to execute
|
- **uv**: Install with `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
||||||
|
- **Python 3.11+**: Recommended Python version
|
||||||
|
|
||||||
```v
|
## Examples
|
||||||
|
|
||||||
#!/usr/bin/env -S v -gc none -cc tcc -d use_openssl -enable-globals run
|
See `examples/lang/python/` for complete working examples.
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
import freeflowuniverse.herolib.lang.python
|
- `uv` is 10-100x faster than pip for most operations
|
||||||
import json
|
- Dependency resolution is significantly improved
|
||||||
|
- Lock files ensure reproducible environments
|
||||||
|
- No manual state tracking reduces complexity and errors
|
||||||
pub struct Person {
|
|
||||||
name string
|
|
||||||
age int
|
|
||||||
is_member bool
|
|
||||||
skills []string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
mut py:=python.new(name:'test')! //a python env with name test
|
|
||||||
//py.update()!
|
|
||||||
py.pip("ipython")!
|
|
||||||
|
|
||||||
|
|
||||||
nrcount:=5
|
|
||||||
//this is used in the pythonexample
|
|
||||||
cmd:=$tmpl("pythonexample.py")
|
|
||||||
|
|
||||||
mut res:=""
|
|
||||||
for i in 0..5{
|
|
||||||
println(i)
|
|
||||||
res=py.exec(cmd:cmd)!
|
|
||||||
|
|
||||||
}
|
|
||||||
//res:=py.exec(cmd:cmd)!
|
|
||||||
|
|
||||||
person:=json.decode(Person,res)!
|
|
||||||
println(person)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
example python script which is in the pythonscripts/ dir
|
|
||||||
|
|
||||||
```py
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
for counter in range(1, @nrcount): # Loop from 1 to the specified param
|
|
||||||
print(f"done_{counter}")
|
|
||||||
|
|
||||||
|
|
||||||
# Define a simple Python structure (e.g., a dictionary)
|
|
||||||
example_struct = {
|
|
||||||
"name": "John Doe",
|
|
||||||
"age": @nrcount,
|
|
||||||
"is_member": True,
|
|
||||||
"skills": ["Python", "Data Analysis", "Machine Learning"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Convert the structure to a JSON string
|
|
||||||
json_string = json.dumps(example_struct, indent=4)
|
|
||||||
|
|
||||||
# Print the JSON string
|
|
||||||
print("==RESULT==")
|
|
||||||
print(json_string)
|
|
||||||
```
|
|
||||||
|
|
||||||
> see `herolib/examples/lang/python/pythonexample.vsh`
|
|
||||||
|
|
||||||
|
|
||||||
## remark
|
|
||||||
|
|
||||||
This is a slow way how to execute python, is about 2 per second on a fast machine, need to implement something where we keep the python in mem and reading from a queue e.g. redis this will go much faster, but ok for now.
|
|
||||||
|
|
||||||
see also examples dir, there is a working example
|
|
||||||
|
|
||||||
@@ -1,14 +1,79 @@
|
|||||||
module python
|
module python
|
||||||
|
|
||||||
import freeflowuniverse.herolib.osal.core as osal
|
import freeflowuniverse.herolib.osal.core as osal
|
||||||
import freeflowuniverse.herolib.core.texttools
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
pub fn (py PythonEnv) shell(name_ string) ! {
|
// Open an interactive shell in the Python environment
|
||||||
_ := texttools.name_fix(name_)
|
pub fn (py PythonEnv) shell() ! {
|
||||||
|
console.print_green('Opening interactive shell for Python environment: ${py.name}')
|
||||||
cmd := '
|
cmd := '
|
||||||
cd ${py.path.path}
|
cd ${py.path.path}
|
||||||
source bin/activate
|
source .venv/bin/activate
|
||||||
|
exec \$SHELL
|
||||||
'
|
'
|
||||||
osal.exec(cmd: cmd)!
|
osal.execute_interactive(cmd)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open a Python REPL in the environment
|
||||||
|
pub fn (py PythonEnv) python_shell() ! {
|
||||||
|
console.print_green('Opening Python REPL for environment: ${py.name}')
|
||||||
|
cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
source .venv/bin/activate
|
||||||
|
python
|
||||||
|
'
|
||||||
|
osal.execute_interactive(cmd)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open IPython if available, fallback to regular Python
|
||||||
|
pub fn (py PythonEnv) ipython_shell() ! {
|
||||||
|
console.print_green('Opening IPython shell for environment: ${py.name}')
|
||||||
|
|
||||||
|
// Check if IPython is available
|
||||||
|
check_cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -c "import IPython"
|
||||||
|
'
|
||||||
|
|
||||||
|
check_result := osal.exec(cmd: check_cmd, raise_error: false)!
|
||||||
|
|
||||||
|
mut shell_cmd := ''
|
||||||
|
if check_result.exit_code == 0 {
|
||||||
|
shell_cmd = '
|
||||||
|
cd ${py.path.path}
|
||||||
|
source .venv/bin/activate
|
||||||
|
ipython
|
||||||
|
'
|
||||||
|
} else {
|
||||||
|
console.print_debug('IPython not available, falling back to regular Python shell')
|
||||||
|
shell_cmd = '
|
||||||
|
cd ${py.path.path}
|
||||||
|
source .venv/bin/activate
|
||||||
|
python
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
osal.execute_interactive(shell_cmd)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a specific Python script in the environment
|
||||||
|
pub fn (py PythonEnv) run_script(script_path string) !osal.Job {
|
||||||
|
console.print_debug('Running Python script: ${script_path}')
|
||||||
|
cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
source .venv/bin/activate
|
||||||
|
python ${script_path}
|
||||||
|
'
|
||||||
|
return osal.exec(cmd: cmd)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a uv command in the environment context
|
||||||
|
pub fn (py PythonEnv) uv_run(command string) !osal.Job {
|
||||||
|
console.print_debug('Running uv command: ${command}')
|
||||||
|
cmd := '
|
||||||
|
cd ${py.path.path}
|
||||||
|
uv ${command}
|
||||||
|
'
|
||||||
|
return osal.exec(cmd: cmd)!
|
||||||
|
}
|
||||||
142
lib/lang/python/templates.v
Normal file
142
lib/lang/python/templates.v
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
module python
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.core.texttools
|
||||||
|
import freeflowuniverse.herolib.core.pathlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
pub struct TemplateArgs {
|
||||||
|
pub mut:
|
||||||
|
name string = 'herolib-python-project'
|
||||||
|
version string = '0.1.0'
|
||||||
|
description string = 'A Python project managed by Herolib'
|
||||||
|
python_version string = '3.11'
|
||||||
|
dependencies []string
|
||||||
|
dev_dependencies []string
|
||||||
|
scripts map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate_pyproject_toml creates a pyproject.toml file from template
|
||||||
|
pub fn (mut py PythonEnv) generate_pyproject_toml(args TemplateArgs) ! {
|
||||||
|
template_path := '${@VMODROOT}/lang/python/templates/pyproject.toml'
|
||||||
|
mut template_content := os.read_file(template_path)!
|
||||||
|
|
||||||
|
// Format dependencies
|
||||||
|
mut deps := []string{}
|
||||||
|
for dep in args.dependencies {
|
||||||
|
deps << ' "${dep}",'
|
||||||
|
}
|
||||||
|
dependencies_str := deps.join('\n')
|
||||||
|
|
||||||
|
// Format dev dependencies
|
||||||
|
mut dev_deps := []string{}
|
||||||
|
for dep in args.dev_dependencies {
|
||||||
|
dev_deps << ' "${dep}",'
|
||||||
|
}
|
||||||
|
dev_dependencies_str := dev_deps.join('\n')
|
||||||
|
|
||||||
|
// Format scripts
|
||||||
|
mut scripts := []string{}
|
||||||
|
for name, command in args.scripts {
|
||||||
|
scripts << '${name} = "${command}"'
|
||||||
|
}
|
||||||
|
scripts_str := scripts.join('\n')
|
||||||
|
|
||||||
|
// Replace template variables
|
||||||
|
content := template_content
|
||||||
|
.replace('@{name}', args.name)
|
||||||
|
.replace('@{version}', args.version)
|
||||||
|
.replace('@{description}', args.description)
|
||||||
|
.replace('@{python_version}', args.python_version)
|
||||||
|
.replace('@{dependencies}', dependencies_str)
|
||||||
|
.replace('@{dev_dependencies}', dev_dependencies_str)
|
||||||
|
.replace('@{scripts}', scripts_str)
|
||||||
|
|
||||||
|
// Write to project directory
|
||||||
|
mut pyproject_file := py.path.file_get_new('pyproject.toml')!
|
||||||
|
pyproject_file.write(content)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate_env_script creates an env.sh script from template
|
||||||
|
pub fn (mut py PythonEnv) generate_env_script(args TemplateArgs) ! {
|
||||||
|
template_path := '${@VMODROOT}/lang/python/templates/env.sh'
|
||||||
|
mut template_content := os.read_file(template_path)!
|
||||||
|
|
||||||
|
content := template_content
|
||||||
|
.replace('@{python_version}', args.python_version)
|
||||||
|
|
||||||
|
mut env_file := py.path.file_get_new('env.sh')!
|
||||||
|
env_file.write(content)!
|
||||||
|
os.chmod(env_file.path, 0o755)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate_install_script creates an install.sh script from template
|
||||||
|
pub fn (mut py PythonEnv) generate_install_script(args TemplateArgs) ! {
|
||||||
|
template_path := '${@VMODROOT}/lang/python/templates/install.sh'
|
||||||
|
mut template_content := os.read_file(template_path)!
|
||||||
|
|
||||||
|
content := template_content
|
||||||
|
.replace('@{name}', args.name)
|
||||||
|
.replace('@{python_version}', args.python_version)
|
||||||
|
|
||||||
|
mut install_file := py.path.file_get_new('install.sh')!
|
||||||
|
install_file.write(content)!
|
||||||
|
os.chmod(install_file.path, 0o755)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate_readme creates a basic README.md file
|
||||||
|
pub fn (mut py PythonEnv) generate_readme(args TemplateArgs) ! {
|
||||||
|
readme_content := '# ${args.name}
|
||||||
|
|
||||||
|
${args.description}
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Run the installation script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install uv if not already installed
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
|
||||||
|
# Create and activate environment
|
||||||
|
uv venv --python ${args.python_version}
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Activate the environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source env.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Production
|
||||||
|
${if args.dependencies.len > 0 { '- ' + args.dependencies.join('\n- ') } else { 'None' }}
|
||||||
|
|
||||||
|
### Development
|
||||||
|
${if args.dev_dependencies.len > 0 { '- ' + args.dev_dependencies.join('\n- ') } else { 'None' }}
|
||||||
|
'
|
||||||
|
|
||||||
|
mut readme_file := py.path.file_get_new('README.md')!
|
||||||
|
readme_file.write(readme_content)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate_all_templates creates all template files for the Python environment
|
||||||
|
pub fn (mut py PythonEnv) generate_all_templates(args TemplateArgs) ! {
|
||||||
|
py.generate_pyproject_toml(args)!
|
||||||
|
py.generate_env_script(args)!
|
||||||
|
py.generate_install_script(args)!
|
||||||
|
py.generate_readme(args)!
|
||||||
|
}
|
||||||
10
lib/lang/python/templates/openrpc/env.sh → lib/lang/python/templates/env.sh
Executable file → Normal file
10
lib/lang/python/templates/openrpc/env.sh → lib/lang/python/templates/env.sh
Executable file → Normal file
@@ -1,6 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
# Get the directory where this script is located
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
# Check if uv is installed
|
# Check if uv is installed
|
||||||
if ! command -v uv &> /dev/null; then
|
if ! command -v uv &> /dev/null; then
|
||||||
@@ -15,7 +17,7 @@ echo "✅ uv found: $(uv --version)"
|
|||||||
# Create virtual environment if it doesn't exist
|
# Create virtual environment if it doesn't exist
|
||||||
if [ ! -d ".venv" ]; then
|
if [ ! -d ".venv" ]; then
|
||||||
echo "📦 Creating Python virtual environment..."
|
echo "📦 Creating Python virtual environment..."
|
||||||
uv venv
|
uv venv --python @{python_version}
|
||||||
echo "✅ Virtual environment created"
|
echo "✅ Virtual environment created"
|
||||||
else
|
else
|
||||||
echo "✅ Virtual environment already exists"
|
echo "✅ Virtual environment already exists"
|
||||||
@@ -29,5 +31,7 @@ source .venv/bin/activate
|
|||||||
|
|
||||||
echo "✅ Virtual environment activated"
|
echo "✅ Virtual environment activated"
|
||||||
|
|
||||||
|
# Sync dependencies
|
||||||
|
echo "📦 Installing dependencies with uv..."
|
||||||
uv sync
|
uv sync
|
||||||
|
echo "✅ Dependencies installed"
|
||||||
42
lib/lang/python/templates/install.sh
Normal file
42
lib/lang/python/templates/install.sh
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Python Environment Installation Script
|
||||||
|
# This script sets up the necessary environment for the Python project.
|
||||||
|
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Script directory
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
echo -e "${BLUE}🔧 Setting up @{name} Python Environment${NC}"
|
||||||
|
echo "=================================================="
|
||||||
|
|
||||||
|
# Check if uv is installed
|
||||||
|
if ! command -v uv &> /dev/null; then
|
||||||
|
echo -e "${YELLOW}⚠️ uv is not installed. Installing uv...${NC}"
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
source $HOME/.cargo/env
|
||||||
|
echo -e "${GREEN}✅ uv installed${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ uv found${NC}"
|
||||||
|
|
||||||
|
# Initialize uv project if not already done
|
||||||
|
if [ ! -f "pyproject.toml" ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ No pyproject.toml found. Initializing uv project...${NC}"
|
||||||
|
uv init --no-readme --python @{python_version}
|
||||||
|
echo -e "${GREEN}✅ uv project initialized${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync dependencies
|
||||||
|
echo -e "${YELLOW}📦 Installing dependencies with uv...${NC}"
|
||||||
|
uv sync
|
||||||
|
echo -e "${GREEN}✅ Dependencies installed${NC}"
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import signal
|
|
||||||
import asyncio
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
import uvicorn
|
|
||||||
from fastapi import FastAPI, Response, WebSocket, WebSocketDisconnect
|
|
||||||
from jsonrpcobjects.objects import (
|
|
||||||
ErrorResponse,
|
|
||||||
Notification,
|
|
||||||
ParamsNotification,
|
|
||||||
ParamsRequest,
|
|
||||||
Request,
|
|
||||||
ResultResponse,
|
|
||||||
)
|
|
||||||
from openrpc import RPCServer
|
|
||||||
|
|
||||||
# ---------- FastAPI + OpenRPC ----------
|
|
||||||
app = FastAPI(title="Calculator JSON-RPC (HTTP + UDS)")
|
|
||||||
RequestType = Union[ParamsRequest, Request, ParamsNotification, Notification]
|
|
||||||
rpc = RPCServer(title="Calculator API", version="1.0.0")
|
|
||||||
|
|
||||||
# Calculator methods
|
|
||||||
@rpc.method()
|
|
||||||
async def add(a: float, b: float) -> float:
|
|
||||||
return a + b
|
|
||||||
|
|
||||||
@rpc.method()
|
|
||||||
async def subtract(a: float, b: float) -> float:
|
|
||||||
return a - b
|
|
||||||
|
|
||||||
@rpc.method()
|
|
||||||
async def multiply(a: float, b: float) -> float:
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
@rpc.method()
|
|
||||||
async def divide(a: float, b: float) -> float:
|
|
||||||
if b == 0:
|
|
||||||
# Keep it simple; library turns this into a JSON-RPC error
|
|
||||||
raise ValueError("Division by zero")
|
|
||||||
return a / b
|
|
||||||
|
|
||||||
# Expose the generated OpenRPC spec as REST (proxy to rpc.discover)
|
|
||||||
@app.get("/openrpc.json")
|
|
||||||
async def openrpc_json() -> Response:
|
|
||||||
req = '{"jsonrpc":"2.0","id":1,"method":"rpc.discover"}'
|
|
||||||
resp = await rpc.process_request_async(req) # JSON string
|
|
||||||
payload = json.loads(resp) # dict with "result"
|
|
||||||
return Response(content=json.dumps(payload["result"]),
|
|
||||||
media_type="application/json")
|
|
||||||
|
|
||||||
# JSON-RPC over WebSocket
|
|
||||||
@app.websocket("/rpc")
|
|
||||||
async def ws_process_rpc(websocket: WebSocket) -> None:
|
|
||||||
await websocket.accept()
|
|
||||||
try:
|
|
||||||
async def _process_rpc(request: str) -> None:
|
|
||||||
json_rpc_response = await rpc.process_request_async(request)
|
|
||||||
if json_rpc_response is not None:
|
|
||||||
await websocket.send_text(json_rpc_response)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
data = await websocket.receive_text()
|
|
||||||
asyncio.create_task(_process_rpc(data))
|
|
||||||
except WebSocketDisconnect:
|
|
||||||
await websocket.close()
|
|
||||||
|
|
||||||
# JSON-RPC over HTTP POST
|
|
||||||
@app.post("/rpc", response_model=Union[ErrorResponse, ResultResponse, None])
|
|
||||||
async def http_process_rpc(request: RequestType) -> Response:
|
|
||||||
json_rpc_response = await rpc.process_request_async(request.model_dump_json())
|
|
||||||
return Response(content=json_rpc_response, media_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- Run BOTH: TCP:7766 and UDS:/tmp/server1 ----------
|
|
||||||
async def serve_both():
|
|
||||||
uds_path = "/tmp/server1"
|
|
||||||
|
|
||||||
# Clean stale socket path (if previous run crashed)
|
|
||||||
try:
|
|
||||||
if os.path.exists(uds_path) and not os.path.isfile(uds_path):
|
|
||||||
os.unlink(uds_path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Create two uvicorn servers sharing the same FastAPI app
|
|
||||||
tcp_config = uvicorn.Config(app=app, host="127.0.0.1", port=7766, log_level="info")
|
|
||||||
uds_config = uvicorn.Config(app=app, uds=uds_path, log_level="info")
|
|
||||||
|
|
||||||
tcp_server = uvicorn.Server(tcp_config)
|
|
||||||
uds_server = uvicorn.Server(uds_config)
|
|
||||||
|
|
||||||
# We'll handle signals ourselves (avoid conflicts between two servers)
|
|
||||||
tcp_server.install_signal_handlers = False
|
|
||||||
uds_server.install_signal_handlers = False
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
def _graceful_shutdown():
|
|
||||||
tcp_server.should_exit = True
|
|
||||||
uds_server.should_exit = True
|
|
||||||
|
|
||||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
||||||
try:
|
|
||||||
loop.add_signal_handler(sig, _graceful_shutdown)
|
|
||||||
except NotImplementedError:
|
|
||||||
# e.g., on Windows; best-effort
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
await asyncio.gather(
|
|
||||||
tcp_server.serve(),
|
|
||||||
uds_server.serve(),
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
# Cleanup the socket file on exit
|
|
||||||
try:
|
|
||||||
if os.path.exists(uds_path) and not os.path.isfile(uds_path):
|
|
||||||
os.unlink(uds_path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(serve_both())
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import signal
|
|
||||||
import asyncio
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
import uvicorn
|
|
||||||
from fastapi import FastAPI, Response, WebSocket, WebSocketDisconnect
|
|
||||||
from jsonrpcobjects.objects import (
|
|
||||||
ErrorResponse,
|
|
||||||
Notification,
|
|
||||||
ParamsNotification,
|
|
||||||
ParamsRequest,
|
|
||||||
Request,
|
|
||||||
ResultResponse,
|
|
||||||
)
|
|
||||||
from openrpc import RPCServer
|
|
||||||
|
|
||||||
# Calculator methods
|
|
||||||
@rpc.method()
|
|
||||||
async def add(a: float, b: float) -> float:
|
|
||||||
return a + b
|
|
||||||
|
|
||||||
@rpc.method()
|
|
||||||
async def subtract(a: float, b: float) -> float:
|
|
||||||
return a - b
|
|
||||||
|
|
||||||
@rpc.method()
|
|
||||||
async def multiply(a: float, b: float) -> float:
|
|
||||||
return a * b
|
|
||||||
|
|
||||||
@rpc.method()
|
|
||||||
async def divide(a: float, b: float) -> float:
|
|
||||||
if b == 0:
|
|
||||||
# Keep it simple; library turns this into a JSON-RPC error
|
|
||||||
raise ValueError("Division by zero")
|
|
||||||
return a / b
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "openrpc-server-1"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Example openrpc server"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.12"
|
|
||||||
dependencies = [
|
|
||||||
"fastapi>=0.104.0",
|
|
||||||
"uvicorn[standard]>=0.24.0",
|
|
||||||
"pydantic>=2.5.0",
|
|
||||||
"httpx>=0.25.0",
|
|
||||||
"fastapi-mcp>=0.1.0",
|
|
||||||
"pydantic-settings>=2.1.0",
|
|
||||||
"python-multipart>=0.0.6",
|
|
||||||
"jinja2>=3.1.2",
|
|
||||||
"click>=8.1.0",
|
|
||||||
"openrpc>=10.4.0"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
[tool.uv]
|
|
||||||
dev-dependencies = [
|
|
||||||
"pytest>=7.4.0",
|
|
||||||
"pytest-asyncio>=0.21.0",
|
|
||||||
"black>=23.0.0",
|
|
||||||
"isort>=5.12.0",
|
|
||||||
"mypy>=1.7.0",
|
|
||||||
]
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e # Exit on any error
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source env.sh
|
|
||||||
|
|
||||||
python main.py
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
HTTP_URL="http://127.0.0.1:7766/rpc"
|
|
||||||
HTTP_SPEC="http://127.0.0.1:7766/openrpc.json"
|
|
||||||
UDS_PATH="/tmp/server1"
|
|
||||||
UDS_URL="http://nothing/rpc"
|
|
||||||
UDS_SPEC="http://nothing/openrpc.json"
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "❌ Test failed: $1"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "🔎 Testing HTTP endpoint..."
|
|
||||||
resp_http=$(curl -s -H 'content-type: application/json' \
|
|
||||||
-d '{"jsonrpc":"2.0","id":1,"method":"add","params":{"a":2,"b":3}}' \
|
|
||||||
"$HTTP_URL")
|
|
||||||
|
|
||||||
val_http=$(echo "$resp_http" | jq -r '.result')
|
|
||||||
[[ "$val_http" == "5.0" ]] || fail "HTTP add(2,3) expected 5, got '$val_http'"
|
|
||||||
|
|
||||||
echo "✅ HTTP add works"
|
|
||||||
|
|
||||||
spec_http=$(curl -s "$HTTP_SPEC" | jq -r '.openrpc')
|
|
||||||
[[ "$spec_http" =~ ^1\..* ]] || fail "HTTP spec invalid"
|
|
||||||
echo "✅ HTTP spec available"
|
|
||||||
|
|
||||||
echo "🔎 Testing UDS endpoint..."
|
|
||||||
resp_uds=$(curl -s --unix-socket "$UDS_PATH" \
|
|
||||||
-H 'content-type: application/json' \
|
|
||||||
-d '{"jsonrpc":"2.0","id":2,"method":"add","params":{"a":10,"b":4}}' \
|
|
||||||
"$UDS_URL")
|
|
||||||
|
|
||||||
val_uds=$(echo "$resp_uds" | jq -r '.result')
|
|
||||||
[[ "$val_uds" == "14.0" ]] || fail "UDS add(10,4) expected 14, got '$val_uds'"
|
|
||||||
|
|
||||||
echo "✅ UDS add works"
|
|
||||||
|
|
||||||
spec_uds=$(curl -s --unix-socket "$UDS_PATH" "$UDS_SPEC" | jq -r '.openrpc')
|
|
||||||
[[ "$spec_uds" =~ ^1\..* ]] || fail "UDS spec invalid"
|
|
||||||
echo "✅ UDS spec available"
|
|
||||||
|
|
||||||
echo "🎉 All tests passed successfully"
|
|
||||||
23
lib/lang/python/templates/pyproject.toml
Normal file
23
lib/lang/python/templates/pyproject.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[project]
|
||||||
|
name = "@{name}"
|
||||||
|
version = "@{version}"
|
||||||
|
description = "@{description}"
|
||||||
|
requires-python = ">=@{python_version}"
|
||||||
|
dependencies = [
|
||||||
|
@{dependencies}
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
@{scripts}
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src"]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
dev-dependencies = [
|
||||||
|
@{dev_dependencies}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user