add djls-django crate (#6)

* add djls-django crate

* rework

* oops

* add check for GDAL and GeoDjango

* lots of things

* remove unused scripts

* move scripts to dedicated mod and make static consts

* inline gdal check

* rename mod

* rename mod

* move server info to consts

* adjust pyproject

* hide rustfmt config

* simplify django setup

* adjust printing
This commit is contained in:
Josh Thomas 2024-12-07 16:02:48 -06:00 committed by GitHub
parent b7a1de98dd
commit fce343f44d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1139 additions and 291 deletions

View file

@ -1,4 +1,11 @@
mod packaging;
mod python;
mod runner;
mod scripts;
pub use python::Python;
pub use crate::packaging::ImportCheck;
pub use crate::python::Python;
pub use crate::python::PythonError;
pub use crate::runner::Runner;
pub use crate::runner::RunnerError;
pub use crate::runner::ScriptRunner;

View file

@ -1,37 +1,18 @@
use crate::python::Python;
use crate::runner::{RunnerError, ScriptRunner};
use crate::scripts;
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::path::PathBuf;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Deserialize)]
pub struct Package {
name: String,
version: String,
location: Option<PathBuf>,
}
impl Package {
fn new(name: String, version: String, location: Option<PathBuf>) -> Self {
Self {
name,
version,
location,
}
}
pub fn name(&self) -> &String {
&self.name
}
pub fn version(&self) -> &String {
&self.version
}
pub fn location(&self) -> &Option<PathBuf> {
&self.location
}
}
impl fmt::Display for Package {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.name, self.version)?;
@ -42,60 +23,12 @@ impl fmt::Display for Package {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Deserialize)]
pub struct Packages(HashMap<String, Package>);
impl Packages {
fn new() -> Self {
Self(HashMap::new())
}
pub fn from_executable(executable: &Path) -> Result<Self, PackagingError> {
let output = Command::new(executable)
.args([
"-c",
r#"
import json
import importlib.metadata
packages = {}
for dist in importlib.metadata.distributions():
try:
packages[dist.metadata["Name"]] = {
"name": dist.metadata["Name"],
"version": dist.version,
"location": dist.locate_file("").parent.as_posix() if dist.locate_file("") else None
}
except Exception:
continue
print(json.dumps(packages))
"#,
])
.output()?;
let output_str = String::from_utf8(output.stdout)?;
let packages_info: serde_json::Value = serde_json::from_str(&output_str)?;
Ok(packages_info
.as_object()
.unwrap()
.iter()
.map(|(name, info)| {
(
name.clone(),
Package {
name: name.clone(),
version: info["version"].as_str().unwrap().to_string(),
location: info["location"].as_str().map(PathBuf::from),
},
)
})
.collect())
}
pub fn has_package(&self, name: &str) -> bool {
self.0.iter().any(|pkg| pkg.1.name == name)
pub fn packages(&self) -> Vec<&Package> {
self.0.values().collect()
}
}
@ -107,7 +40,7 @@ impl FromIterator<(String, Package)> for Packages {
impl fmt::Display for Packages {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut packages: Vec<_> = self.0.values().collect();
let mut packages: Vec<_> = self.packages();
packages.sort_by(|a, b| a.name.cmp(&b.name));
if packages.is_empty() {
@ -121,6 +54,26 @@ impl fmt::Display for Packages {
}
}
#[derive(Debug, Deserialize)]
pub struct ImportCheck {
can_import: bool,
}
impl ScriptRunner for ImportCheck {
const SCRIPT: &'static str = scripts::HAS_IMPORT;
}
impl ImportCheck {
pub fn can_import(&self) -> bool {
self.can_import
}
pub fn check(py: &Python, module: &str) -> Result<bool, RunnerError> {
let result = ImportCheck::run_with_py_args(py, module)?;
Ok(result.can_import)
}
}
#[derive(Debug, thiserror::Error)]
pub enum PackagingError {
#[error("IO error: {0}")]
@ -129,6 +82,15 @@ pub enum PackagingError {
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
#[error(transparent)]
Runner(#[from] Box<RunnerError>),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
}
impl From<RunnerError> for PackagingError {
fn from(err: RunnerError) -> Self {
PackagingError::Runner(Box::new(err))
}
}

View file

@ -1,50 +1,17 @@
use crate::packaging::{Packages, PackagingError};
use crate::runner::{Runner, RunnerError, ScriptRunner};
use crate::scripts;
use serde::Deserialize;
use std::fmt;
use std::path::PathBuf;
use std::process::Command;
use std::path::{Path, PathBuf};
use which::which;
#[derive(Clone, Debug, Deserialize)]
pub struct VersionInfo {
pub major: u8,
pub minor: u8,
pub patch: u8,
pub suffix: Option<String>,
}
impl VersionInfo {
fn new(major: u8, minor: u8, patch: u8, suffix: Option<String>) -> Self {
Self {
major,
minor,
patch,
suffix,
}
}
pub fn from_executable(executable: &PathBuf) -> Result<Self, PythonError> {
let output = Command::new(executable)
.args(["-c", "import sys; print(sys.version.split()[0])"])
.output()?;
let version_str = String::from_utf8(output.stdout)?.trim().to_string();
let parts: Vec<&str> = version_str.split('.').collect();
let major: u8 = parts[0].parse()?;
let minor: u8 = parts[1].parse()?;
let last_part = parts[2];
let (patch_str, suffix) = if last_part.chars().any(|c| !c.is_ascii_digit()) {
let idx = last_part.find(|c: char| !c.is_ascii_digit()).unwrap();
(&last_part[..idx], Some(last_part[idx..].to_string()))
} else {
(last_part, None)
};
let patch: u8 = patch_str.parse()?;
Ok(Self::new(major, minor, patch, suffix))
}
major: u8,
minor: u8,
patch: u8,
suffix: Option<String>,
}
impl fmt::Display for VersionInfo {
@ -69,25 +36,6 @@ pub struct SysconfigPaths {
stdlib: PathBuf,
}
impl SysconfigPaths {
pub fn from_executable(executable: &PathBuf) -> Result<Self, PythonError> {
let output = Command::new(executable)
.args([
"-c",
r#"
import json
import sysconfig
paths = sysconfig.get_paths()
print(json.dumps(paths))
"#,
])
.output()?;
let output_str = String::from_utf8(output.stdout)?;
Ok(serde_json::from_str(&output_str)?)
}
}
impl fmt::Display for SysconfigPaths {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "data: {}", self.data.display())?;
@ -101,7 +49,7 @@ impl fmt::Display for SysconfigPaths {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Deserialize)]
pub struct Python {
version_info: VersionInfo,
sysconfig_paths: SysconfigPaths,
@ -112,124 +60,30 @@ pub struct Python {
packages: Packages,
}
impl Python {
fn new(
version_info: VersionInfo,
sysconfig_paths: SysconfigPaths,
sys_prefix: PathBuf,
sys_base_prefix: PathBuf,
sys_executable: PathBuf,
sys_path: Vec<PathBuf>,
packages: Packages,
) -> Self {
Self {
version_info,
sysconfig_paths,
sys_prefix,
sys_base_prefix,
sys_executable,
sys_path,
packages,
}
}
#[derive(Debug, Deserialize)]
pub struct PythonSetup(Python);
impl ScriptRunner for PythonSetup {
const SCRIPT: &'static str = scripts::PYTHON_SETUP;
}
impl From<PythonSetup> for Python {
fn from(setup: PythonSetup) -> Self {
setup.0
}
}
impl Python {
pub fn initialize() -> Result<Self, PythonError> {
let executable = which("python")?;
let output = Command::new(&executable)
.args([
"-c",
r#"
import sys, json
print(json.dumps({
'prefix': sys.prefix,
'base_prefix': sys.base_prefix,
'executable': sys.executable,
'path': [p for p in sys.path if p],
}))
"#,
])
.output()?;
let output_str = String::from_utf8(output.stdout)?;
let sys_info: serde_json::Value = serde_json::from_str(&output_str)?;
Ok(Self::new(
VersionInfo::from_executable(&executable)?,
SysconfigPaths::from_executable(&executable)?,
PathBuf::from(sys_info["prefix"].as_str().unwrap()),
PathBuf::from(sys_info["base_prefix"].as_str().unwrap()),
PathBuf::from(sys_info["executable"].as_str().unwrap()),
sys_info["path"]
.as_array()
.unwrap()
.iter()
.map(|p| PathBuf::from(p.as_str().unwrap()))
.collect(),
Packages::from_executable(&executable)?,
))
Ok(PythonSetup::run_with_path(&executable)?.into())
}
}
pub fn version_info(&self) -> &VersionInfo {
&self.version_info
}
pub fn sysconfig_paths(&self) -> &SysconfigPaths {
&self.sysconfig_paths
}
pub fn sys_prefix(&self) -> &PathBuf {
&self.sys_prefix
}
pub fn sys_base_prefix(&self) -> &PathBuf {
&self.sys_base_prefix
}
pub fn sys_executable(&self) -> &PathBuf {
impl Runner for Python {
fn get_executable(&self) -> &Path {
&self.sys_executable
}
pub fn sys_path(&self) -> &Vec<PathBuf> {
&self.sys_path
}
pub fn packages(&self) -> &Packages {
&self.packages
}
pub fn run_python(&self, code: &str) -> std::io::Result<String> {
let output = Command::new(self.sys_executable())
.args(["-c", code])
.output()?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Python execution failed: {}", error),
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
pub fn run(&self, command: &str, args: &[&str]) -> std::io::Result<String> {
let output = Command::new(self.sys_executable())
.arg("-m")
.arg(command)
.args(args)
.output()?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Command failed: {}", error),
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
}
impl fmt::Display for Python {
@ -251,15 +105,12 @@ impl fmt::Display for Python {
#[derive(Debug, thiserror::Error)]
pub enum PythonError {
#[error("Failed to locate Python executable: {0}")]
PythonNotFound(#[from] which::Error),
#[error("Python execution failed: {0}")]
Execution(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
@ -268,4 +119,19 @@ pub enum PythonError {
#[error("Integer parsing error: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("Failed to locate Python executable: {0}")]
PythonNotFound(#[from] which::Error),
#[error(transparent)]
Runner(#[from] Box<RunnerError>),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
}
impl From<RunnerError> for PythonError {
fn from(err: RunnerError) -> Self {
PythonError::Runner(Box::new(err))
}
}

View file

@ -0,0 +1,179 @@
use crate::python::{Python, PythonError};
use serde::ser::Error;
use serde::Deserialize;
use std::path::{Path, PathBuf};
use std::process::Command;
pub trait Runner {
fn get_executable(&self) -> &Path;
fn run_module(&self, command: &str) -> std::io::Result<String> {
let output = Command::new(self.get_executable())
.arg("-m")
.arg(command)
.output()?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Command failed: {}", error),
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
fn run_module_with_args(&self, command: &str, args: &[&str]) -> std::io::Result<String> {
let output = Command::new(self.get_executable())
.arg("-m")
.arg(command)
.args(args)
.output()?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Command failed: {}", error),
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
fn run_python_code(&self, code: &str) -> std::io::Result<String> {
let output = Command::new(self.get_executable())
.args(["-c", code])
.output()?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Python execution failed: {}", error),
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
fn run_python_code_with_args(&self, code: &str, args: &str) -> std::io::Result<String> {
let output = Command::new(self.get_executable())
.args(["-c", code, args])
.output()?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Python execution failed: {}", error),
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
fn run_script<T>(&self, script: &str) -> Result<T, serde_json::Error>
where
T: for<'de> Deserialize<'de>,
{
let result = self
.run_python_code(script)
.map_err(|e| serde_json::Error::custom(e.to_string()))?;
serde_json::from_str(&result)
}
fn run_script_with_args<T>(&self, script: &str, args: &str) -> Result<T, serde_json::Error>
where
T: for<'de> Deserialize<'de>,
{
let result = self
.run_python_code_with_args(script, args)
.map_err(|e| serde_json::Error::custom(e.to_string()))?;
serde_json::from_str(&result)
}
}
pub struct SimpleRunner {
executable: PathBuf,
}
impl SimpleRunner {
pub fn new(executable: PathBuf) -> Self {
Self { executable }
}
}
impl Runner for SimpleRunner {
fn get_executable(&self) -> &Path {
&self.executable
}
}
pub trait ScriptRunner: Sized {
const SCRIPT: &'static str;
fn run_with_exe<R: Runner>(runner: &R) -> Result<Self, RunnerError>
where
Self: for<'de> Deserialize<'de>,
{
let result = runner.run_script(Self::SCRIPT).map_err(RunnerError::from)?;
Ok(result)
}
fn run_with_exe_args<R: Runner>(runner: &R, args: &str) -> Result<Self, RunnerError>
where
Self: for<'de> Deserialize<'de>,
{
let result = runner
.run_script_with_args(Self::SCRIPT, args)
.map_err(RunnerError::from)?;
Ok(result)
}
fn run_with_path(executable: &Path) -> Result<Self, RunnerError>
where
Self: for<'de> Deserialize<'de>,
{
let runner = &SimpleRunner::new(executable.to_path_buf());
Self::run_with_exe(runner)
}
fn run_with_path_args(executable: &Path, args: &str) -> Result<Self, RunnerError>
where
Self: for<'de> Deserialize<'de>,
{
let runner = &SimpleRunner::new(executable.to_path_buf());
Self::run_with_exe_args(runner, args)
}
fn run_with_py(python: &Python) -> Result<Self, RunnerError>
where
Self: for<'de> Deserialize<'de>,
{
Self::run_with_exe(python)
}
fn run_with_py_args(python: &Python, args: &str) -> Result<Self, RunnerError>
where
Self: for<'de> Deserialize<'de>,
{
Self::run_with_exe_args(python, args)
}
}
#[derive(Debug, thiserror::Error)]
pub enum RunnerError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
#[error(transparent)]
Python(#[from] PythonError),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
}

View file

@ -0,0 +1,14 @@
#[macro_export]
macro_rules! include_script {
($name:expr) => {
include_str!(concat!(
env!("CARGO_WORKSPACE_DIR"),
"/python/djls/",
$name,
".py"
))
};
}
pub const HAS_IMPORT: &str = include_script!("has_import");
pub const PYTHON_SETUP: &str = include_script!["python_setup"];