using CSnakes.Runtime;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Threading.Tasks;
using TensorStack.Common.Common;
using TensorStack.Python.Common;
using TensorStack.Python.Config;
namespace TensorStack.Python
{
///
/// PythonManager - Manage Python portable installation and virtual environment creation
///
public class PythonManager
{
private readonly ILogger _logger;
private readonly EnvironmentConfig _config;
private readonly string _pythonPath;
private readonly string _pipelinePath;
private readonly string _baseDirectory;
private readonly string _pythonVersion = "3.12.10";
///
/// Initializes a new instance of the class.
///
/// The configuration.
/// The base directory.
/// The logger.
public PythonManager(EnvironmentConfig config, string baseDirectory, ILogger logger = default)
{
_logger = logger;
_config = config;
_baseDirectory = Path.GetFullPath(baseDirectory);
_pythonPath = Path.GetFullPath(Path.Join(_config.Directory, "Python"));
_pipelinePath = Path.GetFullPath(Path.Join(_config.Directory, "Pipelines"));
CopyInternalPythonFiles();
CopyInternalPipelineFiles();
}
///
/// Initializes a new instance of the class.
///
/// The configuration.
/// The logger.
public PythonManager(EnvironmentConfig config, ILogger logger = default)
: this(config, AppDomain.CurrentDomain.BaseDirectory, logger) { }
///
/// Load an existing environment.
///
/// The progress callback.
public async Task LoadAsync(IProgress progressCallback = null)
{
return await LoadInternalAsync(progressCallback);
}
///
/// Creates the Python Virtual Environment.
/// If the environment already exists it is loaded after package manager is run (update)
///
/// Delete and rebuild the environment
/// Delete and rebuild the environment and base Python installation
public Task CreateAsync(EnvironmentMode mode, IProgress progressCallback = null)
{
return Task.Run(async () =>
{
var isRebuild = mode == EnvironmentMode.Rebuild;
var isReinstall = mode == EnvironmentMode.Reinstall;
await DownloadAsync(isReinstall, progressCallback);
if (isReinstall || isRebuild)
await DeleteAsync();
return await CreateInternalAsync(progressCallback);
});
}
///
/// Delete an environment
///
private async Task DeleteAsync()
{
var path = Path.Combine(_pipelinePath, $".{_config.Environment}");
if (!Directory.Exists(path))
return false;
await Task.Run(() => Directory.Delete(path, true));
return Exists();
}
///
/// Checks if a environment exists
///
/// The name.
public bool Exists()
{
var path = Path.Combine(_pipelinePath, $".{_config.Environment}");
return Directory.Exists(path);
}
///
/// Creates an environment.
///
private async Task CreateInternalAsync(IProgress progressCallback = null)
{
var requirementsFile = Path.Combine(_pipelinePath, "requirements.txt");
try
{
progressCallback.SendMessage($"Creating Python Virtual Environment (.{_config.Environment})");
await File.WriteAllLinesAsync(requirementsFile, _config.Requirements);
var environment = PythonEnvironmentHelper.CreateEnvironment(_config.Environment, _pythonPath, _pipelinePath, requirementsFile, _pythonVersion, _logger);
progressCallback.SendMessage($"Python Virtual Environment Created");
return environment;
}
finally
{
FileHelper.DeleteFile(requirementsFile);
}
}
///
/// Load an existing Environment.
///
/// The progress callback.
/// A Task<IPythonEnvironment> representing the asynchronous operation.
/// Environment does not exist
private async Task LoadInternalAsync(IProgress progressCallback = null)
{
if (!Exists())
throw new Exception("Environment does not exist");
return await Task.Run(() =>
{
progressCallback.SendMessage($"Loading Python Virtual Environment (.{_config.Environment})");
var environment = PythonEnvironmentHelper.CreateEnvironment(_config.Environment, _pythonPath, _pipelinePath, _pythonVersion, _logger);
progressCallback.SendMessage($"Python Virtual Environment Loaded");
return environment;
});
}
///
/// Downloads and installs Win-Python portable v3.12.10.
///
/// if set to true [reinstall].
private async Task DownloadAsync(bool reinstall, IProgress progressCallback = null)
{
var subfolder = "WPy64-312100/python";
var exePath = Path.Combine(_pythonPath, "python.exe");
var downloadPath = Path.Combine(_pythonPath, "Winpython64-3.12.10.0dot.zip");
var pythonUrl = "https://github.com/winpython/winpython/releases/download/15.3.20250425final/Winpython64-3.12.10.0dot.zip";
if (reinstall)
{
progressCallback.SendMessage($"Reinstalling Python {_pythonVersion}...");
if (File.Exists(downloadPath))
File.Delete(downloadPath);
if (Directory.Exists(_pythonPath))
Directory.Delete(_pythonPath, true);
progressCallback.SendMessage($"Python Uninstalled.");
}
// Create Python
Directory.CreateDirectory(_pythonPath);
// Download Python
if (!File.Exists(downloadPath))
{
progressCallback.SendMessage($"Download Python {_pythonVersion}...");
using (var httpClient = new HttpClient())
using (var response = await httpClient.GetAsync(pythonUrl))
{
response.EnsureSuccessStatusCode();
using (var stream = new FileStream(downloadPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await response.Content.CopyToAsync(stream);
}
}
progressCallback.SendMessage("Python Download Complete.");
}
// Extract ZIP file
if (!File.Exists(exePath))
{
progressCallback.SendMessage($"Installing Python {_pythonVersion}...");
CopyInternalPythonFiles();
using (var archive = ZipFile.OpenRead(downloadPath))
{
foreach (var entry in archive.Entries)
{
if (entry.FullName.StartsWith(subfolder, StringComparison.OrdinalIgnoreCase))
{
var relativePath = entry.FullName.Replace('/', '\\').Substring(subfolder.Length);
if (string.IsNullOrWhiteSpace(relativePath))
continue;
var isDirectory = relativePath.EndsWith('\\');
var destinationPath = Path.Combine(_pythonPath, relativePath.TrimStart('\\').TrimEnd('\\'));
if (isDirectory)
{
Directory.CreateDirectory(destinationPath);
continue;
}
entry.ExtractToFile(destinationPath, overwrite: true);
}
}
}
progressCallback.SendMessage($"Python Install Complete.");
}
}
///
/// Copies the internal python files.
///
private void CopyInternalPythonFiles()
{
Directory.CreateDirectory(_pythonPath);
CopyFiles(Path.Combine(_baseDirectory, "Python"), _pythonPath);
}
///
/// Copies the internal pipeline files.
///
private void CopyInternalPipelineFiles()
{
Directory.CreateDirectory(_pipelinePath);
CopyFiles(Path.Combine(_baseDirectory, "Pipelines"), _pipelinePath);
}
///
/// Copies the files from source to target.
///
/// The source path.
/// The target path.
private static void CopyFiles(string sourcePath, string targetPath)
{
foreach (var dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath));
foreach (var sourceFile in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
{
var targetFile = sourceFile.Replace(sourcePath, targetPath);
if (!File.Exists(targetFile) || File.GetLastWriteTimeUtc(sourceFile) > File.GetLastWriteTimeUtc(targetFile))
{
File.Copy(sourceFile, targetFile, true);
}
}
}
}
}