move GIS check to Python agent (#29)

This commit is contained in:
Josh Thomas 2024-12-12 23:32:52 -06:00 committed by GitHub
parent b993e35460
commit cff90ee869
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 80 additions and 102 deletions

View file

@ -1,5 +1,3 @@
use djls_ipc::v1::*;
use djls_ipc::{ProcessError, PythonProcess};
use std::fmt; use std::fmt;
#[derive(Debug)] #[derive(Debug)]
@ -36,24 +34,6 @@ impl Apps {
pub fn iter(&self) -> impl Iterator<Item = &App> { pub fn iter(&self) -> impl Iterator<Item = &App> {
self.apps().iter() self.apps().iter()
} }
pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result<bool, ProcessError> {
let request = messages::Request {
command: Some(messages::request::Command::CheckAppInstalled(
check::AppInstalledRequest {
app_name: app.to_string(),
},
)),
};
let response = python.send(request).map_err(ProcessError::Transport)?;
match response.result {
Some(messages::response::Result::CheckAppInstalled(response)) => Ok(response.passed),
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
_ => Err(ProcessError::Response),
}
}
} }
impl fmt::Display for Apps { impl fmt::Display for Apps {

View file

@ -1,4 +1,3 @@
use crate::gis::{check_gis_setup, GISError};
use djls_ipc::v1::*; use djls_ipc::v1::*;
use djls_ipc::IpcCommand; use djls_ipc::IpcCommand;
use djls_ipc::{ProcessError, PythonProcess, TransportError}; use djls_ipc::{ProcessError, PythonProcess, TransportError};
@ -24,9 +23,13 @@ impl DjangoProject {
pub fn setup(mut python: PythonProcess) -> Result<Self, ProjectError> { pub fn setup(mut python: PythonProcess) -> Result<Self, ProjectError> {
let py = Python::setup(&mut python)?; let py = Python::setup(&mut python)?;
if !check_gis_setup(&mut python)? { match check::GeoDjangoPrereqsRequest::execute(&mut python)?.result {
Some(messages::response::Result::CheckGeodjangoPrereqs(response)) => {
if !response.passed {
eprintln!("Warning: GeoDjango detected but GDAL is not available."); eprintln!("Warning: GeoDjango detected but GDAL is not available.");
eprintln!("Django initialization will be skipped. Some features may be limited."); eprintln!(
"Django initialization will be skipped. Some features may be limited."
);
eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites."); eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites.");
return Ok(Self { return Ok(Self {
@ -35,6 +38,10 @@ impl DjangoProject {
version: String::new(), version: String::new(),
}); });
} }
}
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message))?,
_ => Err(ProcessError::Response)?,
}
let response = django::GetProjectInfoRequest::execute(&mut python)?; let response = django::GetProjectInfoRequest::execute(&mut python)?;
@ -77,8 +84,6 @@ pub enum ProjectError {
DjangoNotFound, DjangoNotFound,
#[error("IO error: {0}")] #[error("IO error: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("GIS error: {0}")]
Gis(#[from] GISError),
#[error("JSON parsing error: {0}")] #[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error(transparent)] #[error(transparent)]

View file

@ -1,26 +0,0 @@
use crate::apps::Apps;
use djls_ipc::{ProcessError, PythonProcess, TransportError};
use std::process::Command;
pub fn check_gis_setup(python: &mut PythonProcess) -> Result<bool, GISError> {
let has_geodjango = Apps::check_installed(python, "django.contrib.gis")?;
let gdal_is_installed = Command::new("gdalinfo")
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false);
Ok(!has_geodjango || gdal_is_installed)
}
#[derive(Debug, thiserror::Error)]
pub enum GISError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
#[error("Process error: {0}")]
Process(#[from] ProcessError),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
}

View file

@ -1,6 +1,5 @@
mod apps; mod apps;
mod django; mod django;
mod gis;
mod templates; mod templates;
pub use django::DjangoProject; pub use django::DjangoProject;

View file

@ -29,6 +29,22 @@ impl IpcCommand for v1::check::HealthRequest {
} }
} }
impl IpcCommand for v1::check::GeoDjangoPrereqsRequest {
fn into_request(&self) -> messages::Request {
messages::Request {
command: Some(messages::request::Command::CheckGeodjangoPrereqs(*self)),
}
}
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError> {
match response.result {
Some(messages::response::Result::CheckGeodjangoPrereqs(_)) => Ok(response),
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
_ => Err(ProcessError::Response),
}
}
}
impl IpcCommand for v1::python::GetEnvironmentRequest { impl IpcCommand for v1::python::GetEnvironmentRequest {
fn into_request(&self) -> messages::Request { fn into_request(&self) -> messages::Request {
messages::Request { messages::Request {

View file

@ -8,10 +8,8 @@ message HealthResponse {
optional string error = 2; optional string error = 2;
} }
message AppInstalledRequest { message GeoDjangoPrereqsRequest {}
string app_name = 1; message GeoDjangoPrereqsResponse {
}
message AppInstalledResponse {
bool passed = 1; bool passed = 1;
optional string error = 2; optional string error = 2;
} }

View file

@ -9,7 +9,7 @@ import "v1/python.proto";
message Request { message Request {
oneof command { oneof command {
check.HealthRequest check__health = 1; check.HealthRequest check__health = 1;
check.AppInstalledRequest check__app_installed = 2; check.GeoDjangoPrereqsRequest check__geodjango_prereqs = 2;
python.GetEnvironmentRequest python__get_environment = 1000; python.GetEnvironmentRequest python__get_environment = 1000;
django.GetProjectInfoRequest django__get_project_info = 2000; django.GetProjectInfoRequest django__get_project_info = 2000;
} }
@ -18,7 +18,7 @@ message Request {
message Response { message Response {
oneof result { oneof result {
check.HealthResponse check__health = 1; check.HealthResponse check__health = 1;
check.AppInstalledResponse check__app_installed = 2; check.GeoDjangoPrereqsResponse check__geodjango_prereqs = 2;
python.GetEnvironmentResponse python__get_environment = 1000; python.GetEnvironmentResponse python__get_environment = 1000;
django.GetProjectInfoResponse django__get_project_info = 2000; django.GetProjectInfoResponse django__get_project_info = 2000;
Error error = 9000; Error error = 9000;

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import importlib.metadata import importlib.metadata
import inspect import inspect
import os import os
import subprocess
import sys import sys
import sysconfig import sysconfig
import traceback import traceback
@ -91,16 +92,23 @@ async def check__health(_request: check_pb2.HealthRequest) -> check_pb2.HealthRe
return check_pb2.HealthResponse(passed=True) return check_pb2.HealthResponse(passed=True)
@proto_handler( @proto_handler(check_pb2.GeoDjangoPrereqsRequest)
check_pb2.AppInstalledRequest, async def check__geodjango_prereqs(
error=messages_pb2.Error( request: check_pb2.GeoDjangoPrereqsRequest,
code=messages_pb2.Error.DJANGO_ERROR, message="App is not in INSTALLED_APPS" ) -> check_pb2.GeoDjangoPrereqsResponse:
), has_geodjango = apps.is_installed("django.contrib.gis")
)
async def check__app_installed( try:
request: check_pb2.AppInstalledRequest, gdal_process = subprocess.run(
) -> check_pb2.AppInstalledResponse: ["gdalinfo", "--version"], capture_output=True, check=False
return check_pb2.AppInstalledResponse(passed=apps.is_installed(request.app_name)) )
gdal_is_installed = gdal_process.returncode == 0
except FileNotFoundError:
gdal_is_installed = False
return check_pb2.GeoDjangoPrereqsResponse(
passed=(not has_geodjango) or gdal_is_installed
)
@proto_handler(python_pb2.GetEnvironmentRequest) @proto_handler(python_pb2.GetEnvironmentRequest)

View file

@ -28,7 +28,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ev1/check.proto\x12\rdjls.v1.check\"\x0f\n\rHealthRequest\">\n\x0eHealthResponse\x12\x0e\n\x06passed\x18\x01 \x01(\x08\x12\x12\n\x05\x65rror\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\"\'\n\x13\x41ppInstalledRequest\x12\x10\n\x08\x61pp_name\x18\x01 \x01(\t\"D\n\x14\x41ppInstalledResponse\x12\x0e\n\x06passed\x18\x01 \x01(\x08\x12\x12\n\x05\x65rror\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_errorb\x06proto3') DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ev1/check.proto\x12\rdjls.v1.check\"\x0f\n\rHealthRequest\">\n\x0eHealthResponse\x12\x0e\n\x06passed\x18\x01 \x01(\x08\x12\x12\n\x05\x65rror\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\"\x19\n\x17GeoDjangoPrereqsRequest\"H\n\x18GeoDjangoPrereqsResponse\x12\x0e\n\x06passed\x18\x01 \x01(\x08\x12\x12\n\x05\x65rror\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_errorb\x06proto3')
_globals = globals() _globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@ -39,8 +39,8 @@ if not _descriptor._USE_C_DESCRIPTORS:
_globals['_HEALTHREQUEST']._serialized_end=48 _globals['_HEALTHREQUEST']._serialized_end=48
_globals['_HEALTHRESPONSE']._serialized_start=50 _globals['_HEALTHRESPONSE']._serialized_start=50
_globals['_HEALTHRESPONSE']._serialized_end=112 _globals['_HEALTHRESPONSE']._serialized_end=112
_globals['_APPINSTALLEDREQUEST']._serialized_start=114 _globals['_GEODJANGOPREREQSREQUEST']._serialized_start=114
_globals['_APPINSTALLEDREQUEST']._serialized_end=153 _globals['_GEODJANGOPREREQSREQUEST']._serialized_end=139
_globals['_APPINSTALLEDRESPONSE']._serialized_start=155 _globals['_GEODJANGOPREREQSRESPONSE']._serialized_start=141
_globals['_APPINSTALLEDRESPONSE']._serialized_end=223 _globals['_GEODJANGOPREREQSRESPONSE']._serialized_end=213
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View file

@ -20,13 +20,11 @@ class HealthResponse(_message.Message):
error: str error: str
def __init__(self, passed: bool = ..., error: _Optional[str] = ...) -> None: ... def __init__(self, passed: bool = ..., error: _Optional[str] = ...) -> None: ...
class AppInstalledRequest(_message.Message): class GeoDjangoPrereqsRequest(_message.Message):
__slots__ = ("app_name",) __slots__ = ()
APP_NAME_FIELD_NUMBER: _ClassVar[int] def __init__(self) -> None: ...
app_name: str
def __init__(self, app_name: _Optional[str] = ...) -> None: ...
class AppInstalledResponse(_message.Message): class GeoDjangoPrereqsResponse(_message.Message):
__slots__ = ("passed", "error") __slots__ = ("passed", "error")
PASSED_FIELD_NUMBER: _ClassVar[int] PASSED_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int] ERROR_FIELD_NUMBER: _ClassVar[int]

View file

@ -31,7 +31,7 @@ from . import django_pb2 as v1_dot_django__pb2
from . import python_pb2 as v1_dot_python__pb2 from . import python_pb2 as v1_dot_python__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11v1/messages.proto\x12\x10\x64jls.v1.messages\x1a\x0ev1/check.proto\x1a\x0fv1/django.proto\x1a\x0fv1/python.proto\"\xa6\x02\n\x07Request\x12\x35\n\rcheck__health\x18\x01 \x01(\x0b\x32\x1c.djls.v1.check.HealthRequestH\x00\x12\x42\n\x14\x63heck__app_installed\x18\x02 \x01(\x0b\x32\".djls.v1.check.AppInstalledRequestH\x00\x12I\n\x17python__get_environment\x18\xe8\x07 \x01(\x0b\x32%.djls.v1.python.GetEnvironmentRequestH\x00\x12J\n\x18\x64jango__get_project_info\x18\xd0\x0f \x01(\x0b\x32%.djls.v1.django.GetProjectInfoRequestH\x00\x42\t\n\x07\x63ommand\"\xd5\x02\n\x08Response\x12\x36\n\rcheck__health\x18\x01 \x01(\x0b\x32\x1d.djls.v1.check.HealthResponseH\x00\x12\x43\n\x14\x63heck__app_installed\x18\x02 \x01(\x0b\x32#.djls.v1.check.AppInstalledResponseH\x00\x12J\n\x17python__get_environment\x18\xe8\x07 \x01(\x0b\x32&.djls.v1.python.GetEnvironmentResponseH\x00\x12K\n\x18\x64jango__get_project_info\x18\xd0\x0f \x01(\x0b\x32&.djls.v1.django.GetProjectInfoResponseH\x00\x12)\n\x05\x65rror\x18\xa8\x46 \x01(\x0b\x32\x17.djls.v1.messages.ErrorH\x00\x42\x08\n\x06result\"\xa5\x01\n\x05\x45rror\x12*\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1c.djls.v1.messages.Error.Code\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x11\n\ttraceback\x18\x03 \x01(\t\"L\n\x04\x43ode\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x13\n\x0fINVALID_REQUEST\x10\x01\x12\x10\n\x0cPYTHON_ERROR\x10\x02\x12\x10\n\x0c\x44JANGO_ERROR\x10\x03\x62\x06proto3') DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11v1/messages.proto\x12\x10\x64jls.v1.messages\x1a\x0ev1/check.proto\x1a\x0fv1/django.proto\x1a\x0fv1/python.proto\"\xae\x02\n\x07Request\x12\x35\n\rcheck__health\x18\x01 \x01(\x0b\x32\x1c.djls.v1.check.HealthRequestH\x00\x12J\n\x18\x63heck__geodjango_prereqs\x18\x02 \x01(\x0b\x32&.djls.v1.check.GeoDjangoPrereqsRequestH\x00\x12I\n\x17python__get_environment\x18\xe8\x07 \x01(\x0b\x32%.djls.v1.python.GetEnvironmentRequestH\x00\x12J\n\x18\x64jango__get_project_info\x18\xd0\x0f \x01(\x0b\x32%.djls.v1.django.GetProjectInfoRequestH\x00\x42\t\n\x07\x63ommand\"\xdd\x02\n\x08Response\x12\x36\n\rcheck__health\x18\x01 \x01(\x0b\x32\x1d.djls.v1.check.HealthResponseH\x00\x12K\n\x18\x63heck__geodjango_prereqs\x18\x02 \x01(\x0b\x32\'.djls.v1.check.GeoDjangoPrereqsResponseH\x00\x12J\n\x17python__get_environment\x18\xe8\x07 \x01(\x0b\x32&.djls.v1.python.GetEnvironmentResponseH\x00\x12K\n\x18\x64jango__get_project_info\x18\xd0\x0f \x01(\x0b\x32&.djls.v1.django.GetProjectInfoResponseH\x00\x12)\n\x05\x65rror\x18\xa8\x46 \x01(\x0b\x32\x17.djls.v1.messages.ErrorH\x00\x42\x08\n\x06result\"\xa5\x01\n\x05\x45rror\x12*\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1c.djls.v1.messages.Error.Code\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x11\n\ttraceback\x18\x03 \x01(\t\"L\n\x04\x43ode\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x13\n\x0fINVALID_REQUEST\x10\x01\x12\x10\n\x0cPYTHON_ERROR\x10\x02\x12\x10\n\x0c\x44JANGO_ERROR\x10\x03\x62\x06proto3')
_globals = globals() _globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@ -39,11 +39,11 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'v1.messages_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS: if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None DESCRIPTOR._loaded_options = None
_globals['_REQUEST']._serialized_start=90 _globals['_REQUEST']._serialized_start=90
_globals['_REQUEST']._serialized_end=384 _globals['_REQUEST']._serialized_end=392
_globals['_RESPONSE']._serialized_start=387 _globals['_RESPONSE']._serialized_start=395
_globals['_RESPONSE']._serialized_end=728 _globals['_RESPONSE']._serialized_end=744
_globals['_ERROR']._serialized_start=731 _globals['_ERROR']._serialized_start=747
_globals['_ERROR']._serialized_end=896 _globals['_ERROR']._serialized_end=912
_globals['_ERROR_CODE']._serialized_start=820 _globals['_ERROR_CODE']._serialized_start=836
_globals['_ERROR_CODE']._serialized_end=896 _globals['_ERROR_CODE']._serialized_end=912
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View file

@ -13,30 +13,30 @@ from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Opti
DESCRIPTOR: _descriptor.FileDescriptor DESCRIPTOR: _descriptor.FileDescriptor
class Request(_message.Message): class Request(_message.Message):
__slots__ = ("check__health", "check__app_installed", "python__get_environment", "django__get_project_info") __slots__ = ("check__health", "check__geodjango_prereqs", "python__get_environment", "django__get_project_info")
CHECK__HEALTH_FIELD_NUMBER: _ClassVar[int] CHECK__HEALTH_FIELD_NUMBER: _ClassVar[int]
CHECK__APP_INSTALLED_FIELD_NUMBER: _ClassVar[int] CHECK__GEODJANGO_PREREQS_FIELD_NUMBER: _ClassVar[int]
PYTHON__GET_ENVIRONMENT_FIELD_NUMBER: _ClassVar[int] PYTHON__GET_ENVIRONMENT_FIELD_NUMBER: _ClassVar[int]
DJANGO__GET_PROJECT_INFO_FIELD_NUMBER: _ClassVar[int] DJANGO__GET_PROJECT_INFO_FIELD_NUMBER: _ClassVar[int]
check__health: _check_pb2.HealthRequest check__health: _check_pb2.HealthRequest
check__app_installed: _check_pb2.AppInstalledRequest check__geodjango_prereqs: _check_pb2.GeoDjangoPrereqsRequest
python__get_environment: _python_pb2.GetEnvironmentRequest python__get_environment: _python_pb2.GetEnvironmentRequest
django__get_project_info: _django_pb2.GetProjectInfoRequest django__get_project_info: _django_pb2.GetProjectInfoRequest
def __init__(self, check__health: _Optional[_Union[_check_pb2.HealthRequest, _Mapping]] = ..., check__app_installed: _Optional[_Union[_check_pb2.AppInstalledRequest, _Mapping]] = ..., python__get_environment: _Optional[_Union[_python_pb2.GetEnvironmentRequest, _Mapping]] = ..., django__get_project_info: _Optional[_Union[_django_pb2.GetProjectInfoRequest, _Mapping]] = ...) -> None: ... def __init__(self, check__health: _Optional[_Union[_check_pb2.HealthRequest, _Mapping]] = ..., check__geodjango_prereqs: _Optional[_Union[_check_pb2.GeoDjangoPrereqsRequest, _Mapping]] = ..., python__get_environment: _Optional[_Union[_python_pb2.GetEnvironmentRequest, _Mapping]] = ..., django__get_project_info: _Optional[_Union[_django_pb2.GetProjectInfoRequest, _Mapping]] = ...) -> None: ...
class Response(_message.Message): class Response(_message.Message):
__slots__ = ("check__health", "check__app_installed", "python__get_environment", "django__get_project_info", "error") __slots__ = ("check__health", "check__geodjango_prereqs", "python__get_environment", "django__get_project_info", "error")
CHECK__HEALTH_FIELD_NUMBER: _ClassVar[int] CHECK__HEALTH_FIELD_NUMBER: _ClassVar[int]
CHECK__APP_INSTALLED_FIELD_NUMBER: _ClassVar[int] CHECK__GEODJANGO_PREREQS_FIELD_NUMBER: _ClassVar[int]
PYTHON__GET_ENVIRONMENT_FIELD_NUMBER: _ClassVar[int] PYTHON__GET_ENVIRONMENT_FIELD_NUMBER: _ClassVar[int]
DJANGO__GET_PROJECT_INFO_FIELD_NUMBER: _ClassVar[int] DJANGO__GET_PROJECT_INFO_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int] ERROR_FIELD_NUMBER: _ClassVar[int]
check__health: _check_pb2.HealthResponse check__health: _check_pb2.HealthResponse
check__app_installed: _check_pb2.AppInstalledResponse check__geodjango_prereqs: _check_pb2.GeoDjangoPrereqsResponse
python__get_environment: _python_pb2.GetEnvironmentResponse python__get_environment: _python_pb2.GetEnvironmentResponse
django__get_project_info: _django_pb2.GetProjectInfoResponse django__get_project_info: _django_pb2.GetProjectInfoResponse
error: Error error: Error
def __init__(self, check__health: _Optional[_Union[_check_pb2.HealthResponse, _Mapping]] = ..., check__app_installed: _Optional[_Union[_check_pb2.AppInstalledResponse, _Mapping]] = ..., python__get_environment: _Optional[_Union[_python_pb2.GetEnvironmentResponse, _Mapping]] = ..., django__get_project_info: _Optional[_Union[_django_pb2.GetProjectInfoResponse, _Mapping]] = ..., error: _Optional[_Union[Error, _Mapping]] = ...) -> None: ... def __init__(self, check__health: _Optional[_Union[_check_pb2.HealthResponse, _Mapping]] = ..., check__geodjango_prereqs: _Optional[_Union[_check_pb2.GeoDjangoPrereqsResponse, _Mapping]] = ..., python__get_environment: _Optional[_Union[_python_pb2.GetEnvironmentResponse, _Mapping]] = ..., django__get_project_info: _Optional[_Union[_django_pb2.GetProjectInfoResponse, _Mapping]] = ..., error: _Optional[_Union[Error, _Mapping]] = ...) -> None: ...
class Error(_message.Message): class Error(_message.Message):
__slots__ = ("code", "message", "traceback") __slots__ = ("code", "message", "traceback")