Get rid of all transport types and settle on Protobuf (#25)

* Get rid of all transport types and settle on Protobuf

hope i don't regret this

* Update Cargo.toml

* Update agent.py
This commit is contained in:
Josh Thomas 2024-12-12 16:53:49 -06:00 committed by GitHub
parent 643a47953e
commit 0a6e975ca5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1484 additions and 685 deletions

View file

@ -9,7 +9,7 @@ trim_trailing_whitespace = true
[{,.}{j,J}ustfile]
indent_size = 4
[*.{py,rst,ini,md}]
[*.{just,proto,py,rst,ini,md}]
indent_size = 4
[*.py]

View file

@ -22,16 +22,84 @@ fmt:
# Generate protobuf code for both Rust and Python
[no-cd]
gen:
@just proto rust
@just proto py
@just proto rust
@just proto py
# Generate protobuf code for Rust
[no-cd]
rust: check
cargo build -p djls-types
@just proto clean-rust
cargo build -p djls-ipc
[private]
clean-rust:
#!/usr/bin/env python3
from pathlib import Path
import shutil
target_dir = Path("{{ justfile_directory() }}/target")
if target_dir.exists():
for item in target_dir.rglob('*djls[_-]ipc*'):
if item.is_file():
item.unlink()
elif item.is_dir():
shutil.rmtree(item)
# Generate protobuf code for Python
[no-cd]
py: check
protoc -I=proto --python_out=python/djls proto/*.proto
@just proto clean-py
protoc -I=proto \
--python_out=python/djls/proto \
--pyi_out=python/djls/proto \
proto/v1/*.proto
fd -t f "(_pb2\.py|_pb2\.pyi)$" python/djls/proto -x sed -i 's/from v1 import/from . import/g' {}
fd -t d "." python/djls/proto -x touch {}//__init__.py
@just proto py-add-warnings
[private]
clean-py:
#!/usr/bin/env python3
from pathlib import Path
import shutil
proto_dir = Path("{{ justfile_directory() }}/python/djls/proto")
for item in proto_dir.iterdir():
if item.is_file():
item.unlink()
elif item.is_dir():
shutil.rmtree(item)
[private]
py-add-warnings:
#!/usr/bin/env python3
from pathlib import Path
def create_warning(proto_file: str) -> str:
return f'''# WARNING: This file is generated by protobuf. DO NOT EDIT!
# Any changes made to this file will be overwritten when the protobuf files are regenerated.
# Source: {proto_file}
'''
proto_dir = Path("{{ justfile_directory() }}/python/djls/proto")
proto_source_dir = Path("{{ justfile_directory() }}/proto")
proto_sources = {
path.stem: path.relative_to(proto_source_dir)
for path in proto_source_dir.glob('**/*.proto')
}
for file_path in proto_dir.glob("**/*.py*"): # Catches both .py and .pyi in all subdirs
proto_name = file_path.stem.removesuffix('_pb2')
source_proto = proto_sources.get(proto_name)
content = file_path.read_text()
if not content.startswith('# WARNING'):
warning = create_warning(
str(source_proto) if source_proto
else "generated by py-init"
)
file_path.write_text(warning + content)

View file

@ -9,7 +9,6 @@ djls-django = { path = "crates/djls-django" }
djls-ipc = { path = "crates/djls-ipc" }
djls-python = { path = "crates/djls-python" }
djls-server = { path = "crates/djls-server" }
djls-types = { path = "crates/djls-types" }
djls-worker = { path = "crates/djls-worker" }
anyhow = "1.0.94"

View file

@ -8,6 +8,9 @@ mod proto ".just/proto.just"
default:
@just --list
clean:
rm -rf target/
# run pre-commit on all files
lint:
@just --fmt

View file

@ -1,5 +1,7 @@
use clap::{Args, Parser, Subcommand};
use djls_ipc::{PythonProcess, Transport};
use djls_ipc::v1::*;
use djls_ipc::{ProcessError, PythonProcess, TransportError};
use std::ffi::OsStr;
use std::time::Duration;
#[derive(Debug, Parser)]
@ -41,8 +43,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
match cli.command {
Commands::Serve(opts) => {
let python =
PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?;
println!("Starting LSP server...");
let python = PythonProcess::new::<Vec<&OsStr>, &OsStr>(
"djls.agent",
None,
opts.health_check_interval(),
)?;
println!("LSP server started, beginning to serve...");
djls_server::serve(python).await?
}
}

View file

@ -1,5 +1,5 @@
use djls_ipc::{JsonResponse, PythonProcess, TransportError, TransportMessage, TransportResponse};
use serde::Deserialize;
use djls_ipc::v1::*;
use djls_ipc::{ProcessError, PythonProcess};
use std::fmt;
#[derive(Debug)]
@ -20,23 +20,6 @@ impl fmt::Display for App {
#[derive(Debug, Default)]
pub struct Apps(Vec<App>);
#[derive(Debug, Deserialize)]
struct InstalledAppsCheck {
has_app: bool,
}
impl TryFrom<JsonResponse> for InstalledAppsCheck {
type Error = TransportError;
fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
response
.data()
.clone()
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))
}
}
impl Apps {
pub fn from_strings(apps: Vec<String>) -> Self {
Self(apps.into_iter().map(App).collect())
@ -54,18 +37,21 @@ impl Apps {
self.apps().iter()
}
pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result<bool, TransportError> {
let message = TransportMessage::Json("installed_apps_check".to_string());
let response = python.send(message, Some(vec![app.to_string()]))?;
match response {
TransportResponse::Json(json_str) => {
let json_response: JsonResponse = serde_json::from_str(&json_str)?;
let result = InstalledAppsCheck::try_from(json_response)?;
Ok(result.has_app)
}
_ => Err(TransportError::Process(
"Unexpected response type".to_string(),
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),
}
}
}

View file

@ -1,63 +1,26 @@
use crate::apps::Apps;
use crate::gis::{check_gis_setup, GISError};
use crate::templates::TemplateTags;
use djls_ipc::{JsonResponse, PythonProcess, TransportError, TransportMessage, TransportResponse};
use djls_ipc::v1::*;
use djls_ipc::{ProcessError, PythonProcess, TransportError};
use djls_python::{ImportCheck, Python};
use serde::Deserialize;
use std::fmt;
#[derive(Debug)]
pub struct DjangoProject {
py: Python,
python: PythonProcess,
settings_module: String,
installed_apps: Apps,
templatetags: TemplateTags,
}
#[derive(Debug, Deserialize)]
struct DjangoSetup {
installed_apps: Vec<String>,
templatetags: TemplateTags,
}
impl DjangoSetup {
pub fn setup(python: &mut PythonProcess) -> Result<JsonResponse, ProjectError> {
let message = TransportMessage::Json("django_setup".to_string());
let response = python.send(message, None)?;
match response {
TransportResponse::Json(json_str) => {
let json_response: JsonResponse = serde_json::from_str(&json_str)?;
Ok(json_response)
}
_ => Err(ProjectError::Transport(TransportError::Process(
"Unexpected response type".to_string(),
))),
}
}
version: String,
}
impl DjangoProject {
fn new(
py: Python,
python: PythonProcess,
settings_module: String,
installed_apps: Apps,
templatetags: TemplateTags,
) -> Self {
fn new(py: Python, python: PythonProcess, version: String) -> Self {
Self {
py,
python,
settings_module,
installed_apps,
templatetags,
version,
}
}
pub fn setup(mut python: PythonProcess) -> Result<Self, ProjectError> {
let settings_module =
std::env::var("DJANGO_SETTINGS_MODULE").expect("DJANGO_SETTINGS_MODULE must be set");
let py = Python::setup(&mut python)?;
let has_django = ImportCheck::check(&mut python, Some(vec!["django".to_string()]))?;
@ -74,45 +37,52 @@ impl DjangoProject {
return Ok(Self {
py,
python,
settings_module,
installed_apps: Apps::default(),
templatetags: TemplateTags::default(),
version: String::new(),
});
}
let response = DjangoSetup::setup(&mut python)?;
let setup: DjangoSetup = response
.data()
.clone()
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))?;
let request = messages::Request {
command: Some(messages::request::Command::DjangoGetProjectInfo(
django::GetProjectInfoRequest {},
)),
};
Ok(Self::new(
let response = python
.send(request)
.map_err(|e| ProjectError::Transport(e))?;
let version = match response.result {
Some(messages::response::Result::DjangoGetProjectInfo(response)) => {
response.project.unwrap().version
}
Some(messages::response::Result::Error(e)) => {
return Err(ProjectError::Process(ProcessError::Health(e.message)));
}
_ => {
return Err(ProjectError::Process(ProcessError::Response));
}
};
Ok(Self {
py,
python,
settings_module,
Apps::from_strings(setup.installed_apps.to_vec()),
setup.templatetags,
))
version,
})
}
pub fn py(&self) -> &Python {
&self.py
}
fn settings_module(&self) -> &String {
&self.settings_module
fn version(&self) -> &String {
&self.version
}
}
impl fmt::Display for DjangoProject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Django Project")?;
writeln!(f, "Settings Module: {}", self.settings_module)?;
writeln!(f, "Installed Apps:")?;
write!(f, "{}", self.installed_apps)?;
writeln!(f, "Template Tags:")?;
write!(f, "{}", self.templatetags)?;
writeln!(f, "Version: {}", self.version)?;
Ok(())
}
}
@ -121,22 +91,18 @@ impl fmt::Display for DjangoProject {
pub enum ProjectError {
#[error("Django is not installed or cannot be imported")]
DjangoNotFound,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("GIS error: {0}")]
Gis(#[from] GISError),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
#[error(transparent)]
Packaging(#[from] djls_python::PackagingError),
#[error("Process error: {0}")]
Process(#[from] ProcessError),
#[error(transparent)]
Python(#[from] djls_python::PythonError),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
}

View file

@ -1,5 +1,5 @@
use crate::apps::Apps;
use djls_ipc::{PythonProcess, TransportError};
use djls_ipc::{ProcessError, PythonProcess, TransportError};
use std::process::Command;
pub fn check_gis_setup(python: &mut PythonProcess) -> Result<bool, GISError> {
@ -17,10 +17,10 @@ pub fn check_gis_setup(python: &mut PythonProcess) -> Result<bool, GISError> {
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

@ -4,15 +4,16 @@ version = "0.0.0"
edition = "2021"
[dependencies]
djls-types = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
prost = { workspace = true }
bytes = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tempfile = "3.14.0"
bytes = "1.9"
prost = "0.13"
tempfile = "3.14"
[build-dependencies]
prost-build = "0.13"

38
crates/djls-ipc/build.rs Normal file
View file

@ -0,0 +1,38 @@
use std::fs;
use std::path::{Path, PathBuf};
struct Version(&'static str);
impl Version {
fn collect_protos(&self, proto_root: &Path) -> Vec<PathBuf> {
fs::read_dir(proto_root.join(self.0))
.unwrap()
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("proto"))
.map(|entry| entry.path())
.collect()
}
}
const VERSIONS: &[Version] = &[Version("v1")];
fn main() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest_dir.parent().unwrap().parent().unwrap();
let proto_dir = workspace_root.join("proto");
let mut protos = Vec::new();
for version in VERSIONS {
protos.extend(version.collect_protos(&proto_dir));
}
prost_build::Config::new()
.compile_protos(
&protos
.iter()
.map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
&[proto_dir],
)
.unwrap();
}

View file

@ -1,11 +1,9 @@
mod process;
mod proto;
mod transport;
pub use process::ProcessError;
pub use process::PythonProcess;
pub use transport::parse_json_response;
pub use transport::parse_raw_response;
pub use transport::JsonResponse;
pub use proto::v1;
pub use transport::Transport;
pub use transport::TransportError;
pub use transport::TransportMessage;
pub use transport::TransportResponse;

View file

@ -1,6 +1,6 @@
use crate::transport::{
Transport, TransportError, TransportMessage, TransportProtocol, TransportResponse,
};
use crate::proto::v1::*;
use crate::transport::{Transport, TransportError};
use std::ffi::OsStr;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
@ -9,75 +9,130 @@ use tokio::time;
#[derive(Debug)]
pub struct PythonProcess {
transport: Arc<Mutex<Box<dyn TransportProtocol>>>,
transport: Arc<Mutex<Transport>>,
_child: Child,
healthy: Arc<AtomicBool>,
}
impl PythonProcess {
pub fn new(
pub fn new<I, S>(
module: &str,
transport: Transport,
args: Option<I>,
health_check_interval: Option<Duration>,
) -> Result<Self, TransportError> {
let mut child = Command::new("python")
.arg("-m")
.arg(module)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
) -> Result<Self, ProcessError>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut command = Command::new("python");
command.arg("-m").arg(module);
if let Some(args) = args {
command.args(args);
}
command.stdin(Stdio::piped()).stdout(Stdio::piped());
let mut child = command.spawn().map_err(TransportError::Io)?;
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let transport = Transport::new(stdin, stdout)?;
let process = Self {
transport: Arc::new(Mutex::new(transport.create(stdin, stdout)?)),
transport: Arc::new(Mutex::new(transport)),
_child: child,
healthy: Arc::new(AtomicBool::new(true)),
};
if let Some(interval) = health_check_interval {
process.start_health_check_task(interval)?;
let transport = process.transport.clone();
let healthy = process.healthy.clone();
tokio::spawn(async move {
let mut interval = time::interval(interval);
loop {
interval.tick().await;
let _ = PythonProcess::check_health(transport.clone(), healthy.clone()).await;
}
});
}
Ok(process)
}
fn start_health_check_task(&self, interval: Duration) -> Result<(), TransportError> {
let healthy = self.healthy.clone();
let transport = self.transport.clone();
tokio::spawn(async move {
let mut interval = time::interval(interval);
loop {
interval.tick().await;
if let Ok(mut transport) = transport.lock() {
match transport.health_check() {
Ok(()) => {
healthy.store(true, Ordering::SeqCst);
}
Err(_) => {
healthy.store(false, Ordering::SeqCst);
}
}
}
}
});
Ok(())
}
pub fn is_healthy(&self) -> bool {
self.healthy.load(Ordering::SeqCst)
}
pub fn send(
&mut self,
message: TransportMessage,
args: Option<Vec<String>>,
) -> Result<TransportResponse, TransportError> {
request: messages::Request,
) -> Result<messages::Response, TransportError> {
let mut transport = self.transport.lock().unwrap();
transport.send(message, args)
transport.send(request)
}
async fn check_health(
transport: Arc<Mutex<Transport>>,
healthy: Arc<AtomicBool>,
) -> Result<(), ProcessError> {
let request = messages::Request {
command: Some(messages::request::Command::CheckHealth(
check::HealthRequest {},
)),
};
let response = tokio::time::timeout(
Duration::from_secs(5),
tokio::task::spawn_blocking(move || {
let mut transport = transport.lock().unwrap();
transport.send(request)
}),
)
.await
.map_err(|_| ProcessError::Timeout(5))?
.map_err(TransportError::Task)?
.map_err(ProcessError::Transport)?;
let result = match response.result {
Some(messages::response::Result::CheckHealth(health)) => {
if !health.passed {
let error_msg = health.error.unwrap_or_else(|| "Unknown error".to_string());
Err(ProcessError::Health(error_msg))
} else {
Ok(())
}
}
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
_ => Err(ProcessError::Response),
};
healthy.store(result.is_ok(), Ordering::SeqCst);
result
}
}
impl Drop for PythonProcess {
fn drop(&mut self) {
if let Ok(()) = self._child.kill() {
let _ = self._child.wait();
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ProcessError {
#[error("Health check failed: {0}")]
Health(String),
#[error("Operation timed out after {0} seconds")]
Timeout(u64),
#[error("Unexpected response type")]
Response,
#[error("Failed to acquire lock: {0}")]
Lock(String),
#[error("Process not ready: {0}")]
Ready(String),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
}

View file

@ -0,0 +1,17 @@
pub mod v1 {
pub mod messages {
include!(concat!(env!("OUT_DIR"), "/djls.v1.messages.rs"));
}
pub mod check {
include!(concat!(env!("OUT_DIR"), "/djls.v1.check.rs"));
}
pub mod django {
include!(concat!(env!("OUT_DIR"), "/djls.v1.django.rs"));
}
pub mod python {
include!(concat!(env!("OUT_DIR"), "/djls.v1.python.rs"));
}
}

View file

@ -1,320 +1,59 @@
use djls_types::proto::*;
use crate::process::ProcessError;
use crate::proto::v1::*;
use prost::Message;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::Debug;
use std::io::Read;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
use std::process::{ChildStdin, ChildStdout};
use std::sync::{Arc, Mutex};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TransportError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Process error: {0}")]
Process(String),
}
pub enum Transport {
Raw,
Json,
Protobuf,
#[derive(Debug, Clone)]
pub struct Transport {
reader: Arc<Mutex<BufReader<ChildStdout>>>,
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
}
impl Transport {
pub fn create(
&self,
mut stdin: ChildStdin,
mut stdout: ChildStdout,
) -> Result<Box<dyn TransportProtocol>, TransportError> {
let transport_type = match self {
Transport::Raw => "raw",
Transport::Json => "json",
Transport::Protobuf => "protobuf",
};
writeln!(stdin, "{}", transport_type).map_err(TransportError::Io)?;
pub fn new(mut stdin: ChildStdin, mut stdout: ChildStdout) -> Result<Self, ProcessError> {
stdin.flush().map_err(TransportError::Io)?;
let mut ready_line = String::new();
BufReader::new(&mut stdout)
.read_line(&mut ready_line)
.map_err(TransportError::Io)?;
if ready_line.trim() != "ready" {
return Err(TransportError::Process(
"Python process not ready".to_string(),
));
return Err(ProcessError::Ready("Python process not ready".to_string()));
}
match self {
Transport::Raw => Ok(Box::new(RawTransport::new(stdin, stdout)?)),
Transport::Json => Ok(Box::new(JsonTransport::new(stdin, stdout)?)),
Transport::Protobuf => Ok(Box::new(ProtobufTransport::new(stdin, stdout)?)),
}
}
}
#[derive(Debug)]
pub enum TransportMessage {
Raw(String),
Json(String),
Protobuf(ToAgent),
}
#[derive(Debug)]
pub enum TransportResponse {
Raw(String),
Json(String),
Protobuf(FromAgent),
}
pub trait TransportProtocol: Debug + Send {
fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result<Self, TransportError>
where
Self: Sized;
fn health_check(&mut self) -> Result<(), TransportError>;
fn clone_box(&self) -> Box<dyn TransportProtocol>;
fn send_impl(
&mut self,
message: TransportMessage,
args: Option<Vec<String>>,
) -> Result<TransportResponse, TransportError>;
fn send(
&mut self,
message: TransportMessage,
args: Option<Vec<String>>,
) -> Result<TransportResponse, TransportError> {
self.health_check()?;
self.send_impl(message, args)
}
}
impl Clone for Box<dyn TransportProtocol> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[derive(Debug)]
pub struct RawTransport {
reader: Arc<Mutex<BufReader<ChildStdout>>>,
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
}
impl TransportProtocol for RawTransport {
fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result<Self, TransportError> {
Ok(Self {
reader: Arc::new(Mutex::new(BufReader::new(stdout))),
writer: Arc::new(Mutex::new(BufWriter::new(stdin))),
})
}
fn health_check(&mut self) -> Result<(), TransportError> {
self.send_impl(TransportMessage::Raw("health".to_string()), None)
.and_then(|response| match response {
TransportResponse::Raw(s) if s == "ok" => Ok(()),
TransportResponse::Raw(other) => Err(TransportError::Process(format!(
"Health check failed: {}",
other
))),
_ => Err(TransportError::Process(
"Unexpected response type".to_string(),
)),
})
}
fn clone_box(&self) -> Box<dyn TransportProtocol> {
Box::new(RawTransport {
reader: self.reader.clone(),
writer: self.writer.clone(),
})
}
fn send_impl(
pub fn send(
&mut self,
message: TransportMessage,
args: Option<Vec<String>>,
) -> Result<TransportResponse, TransportError> {
let mut writer = self.writer.lock().unwrap();
match message {
TransportMessage::Raw(msg) => {
if let Some(args) = args {
writeln!(writer, "{} {}", msg, args.join(" ")).map_err(TransportError::Io)?;
} else {
writeln!(writer, "{}", msg).map_err(TransportError::Io)?;
}
}
_ => {
return Err(TransportError::Process(
"Raw transport only accepts raw messages".to_string(),
))
}
}
message: messages::Request,
) -> Result<messages::Response, TransportError> {
let buf = message.encode_to_vec();
let mut writer = self.writer.lock().map_err(|_| {
TransportError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to acquire writer lock",
))
})?;
writer
.write_all(&(buf.len() as u32).to_be_bytes())
.map_err(TransportError::Io)?;
writer.write_all(&buf).map_err(TransportError::Io)?;
writer.flush().map_err(TransportError::Io)?;
let mut reader = self.reader.lock().unwrap();
let mut line = String::new();
reader.read_line(&mut line).map_err(TransportError::Io)?;
Ok(TransportResponse::Raw(line.trim().to_string()))
}
}
#[derive(Debug, Serialize, Deserialize)]
struct JsonCommand {
command: String,
args: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JsonResponse {
status: String,
data: Option<Value>,
error: Option<String>,
}
impl JsonResponse {
pub fn data(&self) -> &Option<Value> {
&self.data
}
}
#[derive(Debug)]
pub struct JsonTransport {
reader: Arc<Mutex<BufReader<ChildStdout>>>,
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
}
impl TransportProtocol for JsonTransport {
fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result<Self, TransportError> {
Ok(Self {
reader: Arc::new(Mutex::new(BufReader::new(stdout))),
writer: Arc::new(Mutex::new(BufWriter::new(stdin))),
})
}
fn health_check(&mut self) -> Result<(), TransportError> {
self.send_impl(TransportMessage::Json("health".to_string()), None)
.and_then(|response| match response {
TransportResponse::Json(json) => {
let resp: JsonResponse = serde_json::from_str(&json)?;
match resp.status.as_str() {
"ok" => Ok(()),
_ => Err(TransportError::Process(
resp.error.unwrap_or_else(|| "Unknown error".to_string()),
)),
}
}
_ => Err(TransportError::Process(
"Unexpected response type".to_string(),
)),
})
}
fn clone_box(&self) -> Box<dyn TransportProtocol> {
Box::new(JsonTransport {
reader: self.reader.clone(),
writer: self.writer.clone(),
})
}
fn send_impl(
&mut self,
message: TransportMessage,
args: Option<Vec<String>>,
) -> Result<TransportResponse, TransportError> {
let mut writer = self.writer.lock().unwrap();
match message {
TransportMessage::Json(msg) => {
let command = JsonCommand { command: msg, args };
serde_json::to_writer(&mut *writer, &command)?;
writeln!(writer).map_err(TransportError::Io)?;
}
_ => {
return Err(TransportError::Process(
"JSON transport only accepts JSON messages".to_string(),
))
}
}
writer.flush().map_err(TransportError::Io)?;
let mut reader = self.reader.lock().unwrap();
let mut line = String::new();
reader.read_line(&mut line).map_err(TransportError::Io)?;
Ok(TransportResponse::Json(line.trim().to_string()))
}
}
#[derive(Debug)]
pub struct ProtobufTransport {
reader: Arc<Mutex<BufReader<ChildStdout>>>,
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
}
impl TransportProtocol for ProtobufTransport {
fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result<Self, TransportError> {
Ok(Self {
reader: Arc::new(Mutex::new(BufReader::new(stdout))),
writer: Arc::new(Mutex::new(BufWriter::new(stdin))),
})
}
fn health_check(&mut self) -> Result<(), TransportError> {
let request = ToAgent {
command: Some(to_agent::Command::HealthCheck(HealthCheck {})),
};
match self.send_impl(TransportMessage::Protobuf(request), None)? {
TransportResponse::Protobuf(FromAgent {
message: Some(from_agent::Message::Error(e)),
}) => Err(TransportError::Process(e.message)),
TransportResponse::Protobuf(FromAgent {
message: Some(from_agent::Message::HealthCheck(_)),
}) => Ok(()),
_ => Err(TransportError::Process("Unexpected response".to_string())),
}
}
fn clone_box(&self) -> Box<dyn TransportProtocol> {
Box::new(ProtobufTransport {
reader: self.reader.clone(),
writer: self.writer.clone(),
})
}
fn send_impl(
&mut self,
message: TransportMessage,
_args: Option<Vec<String>>,
) -> Result<TransportResponse, TransportError> {
let mut writer = self.writer.lock().unwrap();
match message {
TransportMessage::Protobuf(msg) => {
let buf = msg.encode_to_vec();
writer
.write_all(&(buf.len() as u32).to_be_bytes())
.map_err(TransportError::Io)?;
writer.write_all(&buf).map_err(TransportError::Io)?;
}
_ => {
return Err(TransportError::Process(
"Protobuf transport only accepts protobuf messages".to_string(),
))
}
}
writer.flush().map_err(TransportError::Io)?;
let mut reader = self.reader.lock().unwrap();
let mut reader = self.reader.lock().map_err(|_| {
TransportError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to acquire reader lock",
))
})?;
let mut length_bytes = [0u8; 4];
reader
.read_exact(&mut length_bytes)
@ -326,17 +65,17 @@ impl TransportProtocol for ProtobufTransport {
.read_exact(&mut message_bytes)
.map_err(TransportError::Io)?;
let response = FromAgent::decode(message_bytes.as_slice())
.map_err(|e| TransportError::Process(e.to_string()))?;
Ok(TransportResponse::Protobuf(response))
messages::Response::decode(message_bytes.as_slice())
.map_err(|e| TransportError::Decode(e.to_string()))
}
}
pub fn parse_raw_response(response: String) -> Result<String, TransportError> {
Ok(response)
}
pub fn parse_json_response(response: String) -> Result<JsonResponse, TransportError> {
serde_json::from_str(&response).map_err(TransportError::Json)
#[derive(thiserror::Error, Debug)]
pub enum TransportError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Task error: {0}")]
Task(#[from] tokio::task::JoinError),
#[error("Failed to decode message: {0}")]
Decode(String),
}

View file

@ -1,4 +1,5 @@
use djls_ipc::{JsonResponse, PythonProcess, TransportError, TransportMessage, TransportResponse};
use djls_ipc::v1::*;
use djls_ipc::{ProcessError, PythonProcess, TransportError};
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt;
@ -6,15 +7,25 @@ use std::path::PathBuf;
#[derive(Clone, Debug, Deserialize)]
pub struct Package {
name: String,
version: String,
location: Option<PathBuf>,
dist_name: String,
dist_version: String,
dist_location: Option<PathBuf>,
}
impl From<python::Package> for Package {
fn from(p: python::Package) -> Self {
Package {
dist_name: p.dist_name,
dist_version: p.dist_version,
dist_location: p.dist_location.map(PathBuf::from),
}
}
}
impl fmt::Display for Package {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.name, self.version)?;
if let Some(location) = &self.location {
write!(f, "{} {}", self.dist_name, self.dist_version)?;
if let Some(location) = &self.dist_location {
write!(f, " ({})", location.display())?;
}
Ok(())
@ -30,6 +41,12 @@ impl Packages {
}
}
impl From<HashMap<String, python::Package>> for Packages {
fn from(packages: HashMap<String, python::Package>) -> Self {
Packages(packages.into_iter().map(|(k, v)| (k, v.into())).collect())
}
}
impl FromIterator<(String, Package)> for Packages {
fn from_iter<T: IntoIterator<Item = (String, Package)>>(iter: T) -> Self {
Self(HashMap::from_iter(iter))
@ -39,7 +56,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.packages();
packages.sort_by(|a, b| a.name.cmp(&b.name));
packages.sort_by(|a, b| a.dist_name.cmp(&b.dist_name));
if packages.is_empty() {
writeln!(f, " (no packages installed)")?;
@ -57,18 +74,6 @@ pub struct ImportCheck {
can_import: bool,
}
impl TryFrom<JsonResponse> for ImportCheck {
type Error = TransportError;
fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
response
.data()
.clone()
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))
}
}
impl ImportCheck {
pub fn can_import(&self) -> bool {
self.can_import
@ -76,19 +81,24 @@ impl ImportCheck {
pub fn check(
python: &mut PythonProcess,
modules: Option<Vec<String>>,
_modules: Option<Vec<String>>,
) -> Result<bool, PackagingError> {
let message = TransportMessage::Json("has_import".to_string());
let response = python.send(message, modules)?;
match response {
TransportResponse::Json(json_str) => {
let json_response: JsonResponse = serde_json::from_str(&json_str)?;
let check = Self::try_from(json_response)?;
Ok(check.can_import)
let request = messages::Request {
command: Some(messages::request::Command::CheckDjangoAvailable(
check::DjangoAvailableRequest {},
)),
};
let response = python
.send(request)
.map_err(|e| PackagingError::Transport(e))?;
match response.result {
Some(messages::response::Result::CheckDjangoAvailable(response)) => Ok(response.passed),
Some(messages::response::Result::Error(e)) => {
Err(PackagingError::Process(ProcessError::Health(e.message)))
}
_ => Err(PackagingError::Transport(TransportError::Process(
"Unexpected response type".to_string(),
))),
_ => Err(PackagingError::Process(ProcessError::Response)),
}
}
}
@ -103,6 +113,8 @@ pub enum PackagingError {
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
#[error("Process error: {0}")]
Process(#[from] ProcessError),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),

View file

@ -1,5 +1,6 @@
use crate::packaging::{Packages, PackagingError};
use djls_ipc::{JsonResponse, PythonProcess, TransportError, TransportMessage, TransportResponse};
use djls_ipc::v1::*;
use djls_ipc::{ProcessError, PythonProcess, TransportError};
use serde::Deserialize;
use std::fmt;
use std::path::PathBuf;
@ -8,16 +9,45 @@ use std::path::PathBuf;
pub struct VersionInfo {
major: u8,
minor: u8,
patch: u8,
suffix: Option<String>,
micro: u8,
releaselevel: ReleaseLevel,
serial: Option<String>,
}
impl From<python::VersionInfo> for VersionInfo {
fn from(v: python::VersionInfo) -> Self {
Self {
major: v.major as u8,
minor: v.minor as u8,
micro: v.micro as u8,
releaselevel: v.releaselevel().into(),
serial: Some(v.serial.to_string()),
}
}
}
#[derive(Clone, Debug, Deserialize)]
pub enum ReleaseLevel {
Alpha,
Beta,
Candidate,
Final,
}
impl From<python::ReleaseLevel> for ReleaseLevel {
fn from(level: python::ReleaseLevel) -> Self {
match level {
python::ReleaseLevel::Alpha => ReleaseLevel::Alpha,
python::ReleaseLevel::Beta => ReleaseLevel::Beta,
python::ReleaseLevel::Candidate => ReleaseLevel::Candidate,
python::ReleaseLevel::Final => ReleaseLevel::Final,
}
}
}
impl fmt::Display for VersionInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
if let Some(suffix) = &self.suffix {
write!(f, "{}", suffix)?;
}
write!(f, "{}.{}.{}", self.major, self.minor, self.micro)?;
Ok(())
}
}
@ -58,30 +88,52 @@ pub struct Python {
packages: Packages,
}
impl TryFrom<JsonResponse> for Python {
type Error = TransportError;
impl Python {
pub fn setup(python: &mut PythonProcess) -> Result<Self, PythonError> {
let request = messages::Request {
command: Some(messages::request::Command::PythonGetEnvironment(
python::GetEnvironmentRequest {},
)),
};
fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
response
.data()
.clone()
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))
let response = python.send(request).map_err(PythonError::Transport)?;
match response.result {
Some(messages::response::Result::PythonGetEnvironment(response)) => response
.python
.ok_or_else(|| PythonError::Process(ProcessError::Response))
.map(Into::into),
Some(messages::response::Result::Error(e)) => {
Err(PythonError::Process(ProcessError::Health(e.message)))
}
_ => Err(PythonError::Process(ProcessError::Response)),
}
}
}
impl Python {
pub fn setup(python: &mut PythonProcess) -> Result<Self, PythonError> {
let message = TransportMessage::Json("python_setup".to_string());
let response = python.send(message, None)?;
match response {
TransportResponse::Json(json_str) => {
let json_response: JsonResponse = serde_json::from_str(&json_str)?;
Ok(Self::try_from(json_response)?)
}
_ => Err(PythonError::Transport(TransportError::Process(
"Unexpected response type".to_string(),
))),
impl From<python::Python> for Python {
fn from(p: python::Python) -> Self {
let sys = p.sys.unwrap();
let sysconfig = p.sysconfig.unwrap();
let site = p.site.unwrap();
Self {
version_info: sys.version_info.unwrap_or_default().into(),
sysconfig_paths: SysconfigPaths {
data: PathBuf::from(sysconfig.data),
include: PathBuf::from(sysconfig.include),
platinclude: PathBuf::from(sysconfig.platinclude),
platlib: PathBuf::from(sysconfig.platlib),
platstdlib: PathBuf::from(sysconfig.platstdlib),
purelib: PathBuf::from(sysconfig.purelib),
scripts: PathBuf::from(sysconfig.scripts),
stdlib: PathBuf::from(sysconfig.stdlib),
},
sys_prefix: PathBuf::from(sys.prefix),
sys_base_prefix: PathBuf::from(sys.base_prefix),
sys_executable: PathBuf::from(sys.executable),
sys_path: sys.path.into_iter().map(PathBuf::from).collect(),
packages: site.packages.into(),
}
}
}
@ -107,25 +159,20 @@ impl fmt::Display for Python {
pub enum PythonError {
#[error("Python execution failed: {0}")]
Execution(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
#[error("Packaging error: {0}")]
Packaging(#[from] PackagingError),
#[error("Integer parsing error: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("Process error: {0}")]
Process(#[from] ProcessError),
#[error("Failed to locate Python executable: {0}")]
PythonNotFound(#[from] which::Error),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
}

View file

@ -1,11 +0,0 @@
[package]
name = "djls-types"
version = "0.0.0"
edition = "2021"
[dependencies]
prost = { workspace = true }
bytes = { workspace = true }
[build-dependencies]
prost-build = "0.13"

View file

@ -1,24 +0,0 @@
use std::fs;
use std::path::PathBuf;
fn main() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest_dir.parent().unwrap().parent().unwrap();
let proto_dir = workspace_root.join("proto");
let protos: Vec<_> = fs::read_dir(&proto_dir)
.unwrap()
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("proto"))
.map(|entry| entry.path())
.collect();
prost_build::compile_protos(
&protos
.iter()
.map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
&[proto_dir],
)
.unwrap();
}

View file

@ -1,5 +0,0 @@
pub mod proto {
include!(concat!(env!("OUT_DIR"), "/djls.rs"));
}
use proto::*;

View file

@ -1,29 +0,0 @@
syntax = "proto3";
package djls;
// Commands we send to Python
message ToAgent {
oneof command {
HealthCheck health_check = 1;
Shutdown shutdown = 2;
}
}
message HealthCheck {}
message Shutdown {}
// Responses we get back
message FromAgent {
oneof message {
HealthCheckResponse health_check = 1;
Error error = 2;
}
}
message HealthCheckResponse {}
message Error {
string message = 1;
string traceback = 2; // Optional stack trace from Python
}

23
proto/v1/check.proto Normal file
View file

@ -0,0 +1,23 @@
syntax = "proto3";
package djls.v1.check;
message HealthRequest {}
message HealthResponse {
bool passed = 1;
optional string error = 2;
}
message DjangoAvailableRequest {}
message DjangoAvailableResponse {
bool passed = 1;
optional string error = 2;
}
message AppInstalledRequest {
string app_name = 1;
}
message AppInstalledResponse {
bool passed = 1;
optional string error = 2;
}

15
proto/v1/django.proto Normal file
View file

@ -0,0 +1,15 @@
syntax = "proto3";
package djls.v1.django;
// models
message Project {
string version = 3;
}
// commands
message GetProjectInfoRequest {}
message GetProjectInfoResponse {
Project project = 1;
}

40
proto/v1/messages.proto Normal file
View file

@ -0,0 +1,40 @@
syntax = "proto3";
package djls.v1.messages;
import "v1/check.proto";
import "v1/django.proto";
import "v1/python.proto";
message Request {
oneof command {
check.HealthRequest check__health = 1;
check.DjangoAvailableRequest check__django_available = 2;
check.AppInstalledRequest check__app_installed = 3;
python.GetEnvironmentRequest python__get_environment = 1000;
django.GetProjectInfoRequest django__get_project_info = 2000;
}
}
message Response {
oneof result {
check.HealthResponse check__health = 1;
check.DjangoAvailableResponse check__django_available = 2;
check.AppInstalledResponse check__app_installed = 3;
python.GetEnvironmentResponse python__get_environment = 1000;
django.GetProjectInfoResponse django__get_project_info = 2000;
Error error = 9000;
}
}
message Error {
Code code = 1;
string message = 2;
string traceback = 3;
enum Code {
UNKNOWN = 0;
INVALID_REQUEST = 1;
PYTHON_ERROR = 2;
DJANGO_ERROR = 3;
}
}

80
proto/v1/python.proto Normal file
View file

@ -0,0 +1,80 @@
syntax = "proto3";
package djls.v1.python;
// models
message Python {
Os os = 1;
Site site = 2;
Sys sys = 3;
Sysconfig sysconfig = 4;
}
message Os {
map<string, string> environ = 1;
}
message Site {
map<string, Package> packages = 1;
}
message Sys {
bool debug_build = 1;
bool dev_mode = 2;
bool is_venv = 3;
string abiflags = 4;
string base_prefix = 5;
string default_encoding = 6;
string executable = 7;
string filesystem_encoding = 8;
string implementation_name = 9;
string platform = 10;
string prefix = 11;
repeated string builtin_module_names = 12;
repeated string dll_paths = 13;
repeated string path = 14;
VersionInfo version_info = 15;
}
message VersionInfo {
uint32 major = 1;
uint32 minor = 2;
uint32 micro = 3;
ReleaseLevel releaselevel = 4;
uint32 serial = 5;
}
enum ReleaseLevel {
ALPHA = 0;
BETA = 1;
CANDIDATE = 2;
FINAL = 3;
}
message Sysconfig {
string data = 1;
string include = 2;
string platinclude = 3;
string platlib = 4;
string platstdlib = 5;
string purelib = 6;
string scripts = 7;
string stdlib = 8;
}
message Package {
string dist_name = 1;
string dist_version = 2;
optional bool dist_editable = 3;
optional string dist_entry_points = 4;
optional string dist_location = 5;
repeated string dist_requires = 6;
optional string dist_requires_python = 7;
}
// commands
message GetEnvironmentRequest {}
message GetEnvironmentResponse {
Python python = 1;
}

12
python/djls/_typing.py Normal file
View file

@ -0,0 +1,12 @@
from __future__ import annotations
import sys
if sys.version_info >= (3, 12):
from typing import override as typing_override
else:
from typing_extensions import (
override as typing_override, # pyright: ignore[reportUnreachable]
)
override = typing_override

131
python/djls/agent.py Normal file
View file

@ -0,0 +1,131 @@
from __future__ import annotations
import logging
import struct
import sys
from google.protobuf.message import Message
from .commands import COMMANDS
from .commands import Command
from .proto.v1 import messages_pb2
logger = logging.getLogger("djls")
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler("/tmp/djls_debug.log")
fh.setLevel(logging.DEBUG)
ch = logging.StreamHandler(sys.stderr)
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
fh.setFormatter(formatter)
ch.setFormatter(formatter)
logger.addHandler(fh)
logger.addHandler(ch)
class LSPAgent:
def __init__(self):
self._commands: dict[str, Command] = {cmd.name: cmd() for cmd in COMMANDS}
logger.debug(
"LSPAgent initialized with commands: %s", list(self._commands.keys())
)
def serve(self):
print("ready", flush=True)
try:
import django
django.setup()
except Exception as e:
error_response = self.create_error(messages_pb2.Error.DJANGO_ERROR, str(e))
self.write_message(error_response)
while True:
try:
data = self.read_message()
if not data:
break
response = self.handle_request(data)
self.write_message(response)
except Exception as e:
error_response = self.create_error(messages_pb2.Error.UNKNOWN, str(e))
self.write_message(error_response)
def read_message(self) -> bytes | None:
length_bytes = sys.stdin.buffer.read(4)
logger.debug("Read length bytes: %r", length_bytes)
if not length_bytes:
return None
length = struct.unpack(">I", length_bytes)[0]
logger.debug("Unpacked length: %d", length)
data = sys.stdin.buffer.read(length)
logger.debug("Read data bytes: %r", data)
return data
def handle_request(self, request_data: bytes) -> Message:
request = messages_pb2.Request()
request.ParseFromString(request_data)
command_name = request.WhichOneof("command")
logger.debug("Command name: %s", command_name)
command = self._commands.get(command_name)
if not command:
logger.error("Unknown command: %s", command_name)
return self.create_error(
messages_pb2.Error.INVALID_REQUEST, f"Unknown command: {command_name}"
)
try:
result = command.execute(getattr(request, command_name))
return messages_pb2.Response(**{command_name: result})
except Exception as e:
logger.exception("Error executing command")
return self.create_error(messages_pb2.Error.UNKNOWN, str(e))
def write_message(self, message: Message) -> None:
data = message.SerializeToString()
logger.debug(f"Sending response, length: {len(data)}, data: {data!r}")
length = struct.pack(">I", len(data))
logger.debug(f"Length bytes: {length!r}")
sys.stdout.buffer.write(length)
sys.stdout.buffer.write(data)
sys.stdout.buffer.flush()
def create_error(
self, code: messages_pb2.Error.Code, message: str
) -> messages_pb2.Response:
response = messages_pb2.Response()
response.error.code = code
response.error.message = message
return response
def main() -> None:
logger.debug("Starting DJLS...")
try:
logger.debug("Initializing LSPAgent...")
agent = LSPAgent()
logger.debug("Starting LSPAgent serve...")
agent.serve()
except KeyboardInterrupt:
logger.debug("Received KeyboardInterrupt")
sys.exit(0)
except Exception as e:
logger.exception("Fatal error")
print(f"error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

209
python/djls/commands.py Normal file
View file

@ -0,0 +1,209 @@
from __future__ import annotations
import importlib.metadata
import os
import sys
import sysconfig
from abc import ABC
from abc import abstractmethod
from typing import ClassVar
from typing import Generic
from typing import TypeVar
from google.protobuf.message import Message
from ._typing import override
from .proto.v1 import check_pb2
from .proto.v1 import django_pb2
from .proto.v1 import python_pb2
Request = TypeVar("Request", bound=Message)
Response = TypeVar("Response", bound=Message)
class Command(ABC, Generic[Request, Response]):
name: ClassVar[str]
request: ClassVar[type[Message]]
response: ClassVar[type[Message]]
def __init_subclass__(cls) -> None:
super().__init_subclass__()
class_vars = ["name", "request", "response"]
for class_var in class_vars:
if not hasattr(cls, class_var):
raise TypeError(
f"Command subclass {cls.__name__} must define '{class_var}'"
)
@abstractmethod
def execute(self, request: Request) -> Response: ...
class CheckHealth(Command[check_pb2.HealthRequest, check_pb2.HealthResponse]):
name = "check__health"
request = check_pb2.HealthRequest
response = check_pb2.HealthResponse
@override
def execute(self, request: check_pb2.HealthRequest) -> check_pb2.HealthResponse:
return check_pb2.HealthResponse(passed=True)
class CheckDjangoAvailable(
Command[check_pb2.DjangoAvailableRequest, check_pb2.DjangoAvailableResponse]
):
name = "check__django_available"
request = check_pb2.DjangoAvailableRequest
response = check_pb2.DjangoAvailableResponse
@override
def execute(
self, request: check_pb2.DjangoAvailableRequest
) -> check_pb2.DjangoAvailableResponse:
try:
import django
return check_pb2.DjangoAvailableResponse(passed=True)
except ImportError:
return check_pb2.DjangoAvailableResponse(
passed=False, error="Django is not installed"
)
class CheckAppInstalled(
Command[check_pb2.AppInstalledRequest, check_pb2.AppInstalledResponse]
):
name = "check__app_installed"
request = check_pb2.AppInstalledRequest
response = check_pb2.AppInstalledResponse
@override
def execute(
self, request: check_pb2.AppInstalledRequest
) -> check_pb2.AppInstalledResponse:
try:
from django.apps import apps
return check_pb2.AppInstalledResponse(
passed=apps.is_installed(request.app_name)
)
except ImportError:
return check_pb2.AppInstalledResponse(
passed=False, error="Django is not installed"
)
class PythonGetEnvironment(
Command[python_pb2.GetEnvironmentRequest, python_pb2.GetEnvironmentResponse]
):
name = "python__get_environment"
request = python_pb2.GetEnvironmentRequest
response = python_pb2.GetEnvironmentResponse
@override
def execute(
self, request: python_pb2.GetEnvironmentRequest
) -> python_pb2.GetEnvironmentResponse:
packages = {}
for dist in importlib.metadata.distributions():
try:
requires = []
try:
requires = list(dist.requires) if hasattr(dist, "requires") else []
except Exception:
pass
location = None
try:
location = str(dist._path) if hasattr(dist, "_path") else None
except Exception:
pass
packages[dist.metadata["Name"]] = python_pb2.Package(
dist_name=dist.metadata["Name"],
dist_version=dist.metadata["Version"],
dist_location=location,
dist_requires=requires,
dist_requires_python=dist.metadata.get("Requires-Python"),
dist_entry_points=str(dist.entry_points)
if hasattr(dist, "entry_points")
else None,
)
except Exception:
continue
sysconfig_paths = sysconfig.get_paths()
version_info = python_pb2.VersionInfo(
major=sys.version_info.major,
minor=sys.version_info.minor,
micro=sys.version_info.micro,
releaselevel={
"alpha": python_pb2.ReleaseLevel.ALPHA,
"beta": python_pb2.ReleaseLevel.BETA,
"candidate": python_pb2.ReleaseLevel.CANDIDATE,
"final": python_pb2.ReleaseLevel.FINAL,
}[sys.version_info.releaselevel],
serial=sys.version_info.serial,
)
return python_pb2.GetEnvironmentResponse(
python=python_pb2.Python(
os=python_pb2.Os(environ={k: v for k, v in os.environ.items()}),
site=python_pb2.Site(packages=packages),
sys=python_pb2.Sys(
debug_build=hasattr(sys, "gettotalrefcount"),
dev_mode=sys.flags.dev_mode,
is_venv=sys.prefix != sys.base_prefix,
abiflags=sys.abiflags,
base_prefix=sys.base_prefix,
default_encoding=sys.getdefaultencoding(),
executable=sys.executable,
filesystem_encoding=sys.getfilesystemencoding(),
implementation_name=sys.implementation.name,
platform=sys.platform,
prefix=sys.prefix,
builtin_module_names=list(sys.builtin_module_names),
dll_paths=sys.path if sys.platform == "win32" else [],
path=sys.path,
version_info=version_info,
),
sysconfig=python_pb2.Sysconfig(
data=sysconfig_paths.get("data", ""),
include=sysconfig_paths.get("include", ""),
platinclude=sysconfig_paths.get("platinclude", ""),
platlib=sysconfig_paths.get("platlib", ""),
platstdlib=sysconfig_paths.get("platstdlib", ""),
purelib=sysconfig_paths.get("purelib", ""),
scripts=sysconfig_paths.get("scripts", ""),
stdlib=sysconfig_paths.get("stdlib", ""),
),
)
)
class DjangoGetProjectInfo(
Command[django_pb2.GetProjectInfoRequest, django_pb2.GetProjectInfoResponse]
):
name = "django__get_project_info"
request = django_pb2.GetProjectInfoRequest
response = django_pb2.GetProjectInfoResponse
@override
def execute(
self, request: django_pb2.GetProjectInfoRequest
) -> django_pb2.GetProjectInfoResponse:
import django
return django_pb2.GetProjectInfoResponse(
project=django_pb2.Project(version=django.__version__)
)
COMMANDS = [
CheckAppInstalled,
CheckDjangoAvailable,
CheckHealth,
PythonGetEnvironment,
DjangoGetProjectInfo,
]

View file

@ -9,34 +9,6 @@ from .scripts import has_import
from .scripts import python_setup
def handle_command(command: str) -> str:
parts = command.strip().split()
command = parts[0]
args = parts[1:] if len(parts) > 1 else []
if command == "django_setup":
return json.dumps(django_setup.get_django_setup_info())
if command == "has_import":
if not args:
return "error: Missing module name argument"
return json.dumps({"can_import": has_import.check_import(args[0])})
if command == "health":
return "ok"
if command == "installed_apps_check":
import django
from django.conf import settings
django.setup()
if not args:
return "error: Missing module name argument"
return json.dumps({"has_app": args[0] in settings.INSTALLED_APPS})
if command == "python_setup":
return json.dumps(python_setup.get_python_info())
if command == "version":
return "0.1.0"
return f"Unknown command: {command}"
def handle_json_command(data: dict[str, Any]) -> dict[str, Any]:
command = data["command"]
args = data.get("args", []) # Get args if they exist
@ -84,20 +56,12 @@ def main():
if not line:
break
if transport_type == "json":
data = json.loads(line)
response = handle_json_command(data)
print(json.dumps(response), flush=True)
else:
command = line.strip()
response = handle_command(command)
print(response, flush=True)
data = json.loads(line)
response = handle_json_command(data)
print(json.dumps(response), flush=True)
except Exception as e:
if transport_type == "json":
print(json.dumps({"status": "error", "error": str(e)}), flush=True)
else:
print(f"error: {str(e)}", flush=True)
print(json.dumps({"status": "error", "error": str(e)}), flush=True)
if __name__ == "__main__":

View file

@ -1,46 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: messages.proto
# Protobuf Python Version: 5.29.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
29,
1,
'',
'messages.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\x04\x64jls\"c\n\x07ToAgent\x12)\n\x0chealth_check\x18\x01 \x01(\x0b\x32\x11.djls.HealthCheckH\x00\x12\"\n\x08shutdown\x18\x02 \x01(\x0b\x32\x0e.djls.ShutdownH\x00\x42\t\n\x07\x63ommand\"\r\n\x0bHealthCheck\"\n\n\x08Shutdown\"g\n\tFromAgent\x12\x31\n\x0chealth_check\x18\x01 \x01(\x0b\x32\x19.djls.HealthCheckResponseH\x00\x12\x1c\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x0b.djls.ErrorH\x00\x42\t\n\x07message\"\x15\n\x13HealthCheckResponse\"+\n\x05\x45rror\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x11\n\ttraceback\x18\x02 \x01(\tb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_TOAGENT']._serialized_start=24
_globals['_TOAGENT']._serialized_end=123
_globals['_HEALTHCHECK']._serialized_start=125
_globals['_HEALTHCHECK']._serialized_end=138
_globals['_SHUTDOWN']._serialized_start=140
_globals['_SHUTDOWN']._serialized_end=150
_globals['_FROMAGENT']._serialized_start=152
_globals['_FROMAGENT']._serialized_end=255
_globals['_HEALTHCHECKRESPONSE']._serialized_start=257
_globals['_HEALTHCHECKRESPONSE']._serialized_end=278
_globals['_ERROR']._serialized_start=280
_globals['_ERROR']._serialized_end=323
# @@protoc_insertion_point(module_scope)

View file

@ -0,0 +1,4 @@
# WARNING: This file is generated by protobuf. DO NOT EDIT!
# Any changes made to this file will be overwritten when the protobuf files are regenerated.
# Source: generated by py-init

View file

@ -0,0 +1,50 @@
# WARNING: This file is generated by protobuf. DO NOT EDIT!
# Any changes made to this file will be overwritten when the protobuf files are regenerated.
# Source: v1/check.proto
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: v1/check.proto
# Protobuf Python Version: 5.29.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
29,
1,
'',
'v1/check.proto'
)
# @@protoc_insertion_point(imports)
_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\"\x18\n\x16\x44jangoAvailableRequest\"G\n\x17\x44jangoAvailableResponse\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')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'v1.check_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_HEALTHREQUEST']._serialized_start=33
_globals['_HEALTHREQUEST']._serialized_end=48
_globals['_HEALTHRESPONSE']._serialized_start=50
_globals['_HEALTHRESPONSE']._serialized_end=112
_globals['_DJANGOAVAILABLEREQUEST']._serialized_start=114
_globals['_DJANGOAVAILABLEREQUEST']._serialized_end=138
_globals['_DJANGOAVAILABLERESPONSE']._serialized_start=140
_globals['_DJANGOAVAILABLERESPONSE']._serialized_end=211
_globals['_APPINSTALLEDREQUEST']._serialized_start=213
_globals['_APPINSTALLEDREQUEST']._serialized_end=252
_globals['_APPINSTALLEDRESPONSE']._serialized_start=254
_globals['_APPINSTALLEDRESPONSE']._serialized_end=322
# @@protoc_insertion_point(module_scope)

View file

@ -0,0 +1,47 @@
# WARNING: This file is generated by protobuf. DO NOT EDIT!
# Any changes made to this file will be overwritten when the protobuf files are regenerated.
# Source: v1/check.proto
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Optional as _Optional
DESCRIPTOR: _descriptor.FileDescriptor
class HealthRequest(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class HealthResponse(_message.Message):
__slots__ = ("passed", "error")
PASSED_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
passed: bool
error: str
def __init__(self, passed: bool = ..., error: _Optional[str] = ...) -> None: ...
class DjangoAvailableRequest(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class DjangoAvailableResponse(_message.Message):
__slots__ = ("passed", "error")
PASSED_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
passed: bool
error: str
def __init__(self, passed: bool = ..., error: _Optional[str] = ...) -> None: ...
class AppInstalledRequest(_message.Message):
__slots__ = ("app_name",)
APP_NAME_FIELD_NUMBER: _ClassVar[int]
app_name: str
def __init__(self, app_name: _Optional[str] = ...) -> None: ...
class AppInstalledResponse(_message.Message):
__slots__ = ("passed", "error")
PASSED_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
passed: bool
error: str
def __init__(self, passed: bool = ..., error: _Optional[str] = ...) -> None: ...

View file

@ -0,0 +1,44 @@
# WARNING: This file is generated by protobuf. DO NOT EDIT!
# Any changes made to this file will be overwritten when the protobuf files are regenerated.
# Source: v1/django.proto
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: v1/django.proto
# Protobuf Python Version: 5.29.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
29,
1,
'',
'v1/django.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fv1/django.proto\x12\x0e\x64jls.v1.django\"\x1a\n\x07Project\x12\x0f\n\x07version\x18\x03 \x01(\t\"\x17\n\x15GetProjectInfoRequest\"B\n\x16GetProjectInfoResponse\x12(\n\x07project\x18\x01 \x01(\x0b\x32\x17.djls.v1.django.Projectb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'v1.django_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_PROJECT']._serialized_start=35
_globals['_PROJECT']._serialized_end=61
_globals['_GETPROJECTINFOREQUEST']._serialized_start=63
_globals['_GETPROJECTINFOREQUEST']._serialized_end=86
_globals['_GETPROJECTINFORESPONSE']._serialized_start=88
_globals['_GETPROJECTINFORESPONSE']._serialized_end=154
# @@protoc_insertion_point(module_scope)

View file

@ -0,0 +1,25 @@
# WARNING: This file is generated by protobuf. DO NOT EDIT!
# Any changes made to this file will be overwritten when the protobuf files are regenerated.
# Source: v1/django.proto
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class Project(_message.Message):
__slots__ = ("version",)
VERSION_FIELD_NUMBER: _ClassVar[int]
version: str
def __init__(self, version: _Optional[str] = ...) -> None: ...
class GetProjectInfoRequest(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class GetProjectInfoResponse(_message.Message):
__slots__ = ("project",)
PROJECT_FIELD_NUMBER: _ClassVar[int]
project: Project
def __init__(self, project: _Optional[_Union[Project, _Mapping]] = ...) -> None: ...

View file

@ -0,0 +1,49 @@
# WARNING: This file is generated by protobuf. DO NOT EDIT!
# Any changes made to this file will be overwritten when the protobuf files are regenerated.
# Source: v1/messages.proto
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: v1/messages.proto
# Protobuf Python Version: 5.29.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
29,
1,
'',
'v1/messages.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from . import check_pb2 as v1_dot_check__pb2
from . import django_pb2 as v1_dot_django__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\"\xf0\x02\n\x07Request\x12\x35\n\rcheck__health\x18\x01 \x01(\x0b\x32\x1c.djls.v1.check.HealthRequestH\x00\x12H\n\x17\x63heck__django_available\x18\x02 \x01(\x0b\x32%.djls.v1.check.DjangoAvailableRequestH\x00\x12\x42\n\x14\x63heck__app_installed\x18\x03 \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\"\xa0\x03\n\x08Response\x12\x36\n\rcheck__health\x18\x01 \x01(\x0b\x32\x1d.djls.v1.check.HealthResponseH\x00\x12I\n\x17\x63heck__django_available\x18\x02 \x01(\x0b\x32&.djls.v1.check.DjangoAvailableResponseH\x00\x12\x43\n\x14\x63heck__app_installed\x18\x03 \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')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'v1.messages_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_REQUEST']._serialized_start=90
_globals['_REQUEST']._serialized_end=458
_globals['_RESPONSE']._serialized_start=461
_globals['_RESPONSE']._serialized_end=877
_globals['_ERROR']._serialized_start=880
_globals['_ERROR']._serialized_end=1045
_globals['_ERROR_CODE']._serialized_start=969
_globals['_ERROR_CODE']._serialized_end=1045
# @@protoc_insertion_point(module_scope)

View file

@ -0,0 +1,63 @@
# WARNING: This file is generated by protobuf. DO NOT EDIT!
# Any changes made to this file will be overwritten when the protobuf files are regenerated.
# Source: v1/messages.proto
from . import check_pb2 as _check_pb2
from . import django_pb2 as _django_pb2
from . import python_pb2 as _python_pb2
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class Request(_message.Message):
__slots__ = ("check__health", "check__django_available", "check__app_installed", "python__get_environment", "django__get_project_info")
CHECK__HEALTH_FIELD_NUMBER: _ClassVar[int]
CHECK__DJANGO_AVAILABLE_FIELD_NUMBER: _ClassVar[int]
CHECK__APP_INSTALLED_FIELD_NUMBER: _ClassVar[int]
PYTHON__GET_ENVIRONMENT_FIELD_NUMBER: _ClassVar[int]
DJANGO__GET_PROJECT_INFO_FIELD_NUMBER: _ClassVar[int]
check__health: _check_pb2.HealthRequest
check__django_available: _check_pb2.DjangoAvailableRequest
check__app_installed: _check_pb2.AppInstalledRequest
python__get_environment: _python_pb2.GetEnvironmentRequest
django__get_project_info: _django_pb2.GetProjectInfoRequest
def __init__(self, check__health: _Optional[_Union[_check_pb2.HealthRequest, _Mapping]] = ..., check__django_available: _Optional[_Union[_check_pb2.DjangoAvailableRequest, _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: ...
class Response(_message.Message):
__slots__ = ("check__health", "check__django_available", "check__app_installed", "python__get_environment", "django__get_project_info", "error")
CHECK__HEALTH_FIELD_NUMBER: _ClassVar[int]
CHECK__DJANGO_AVAILABLE_FIELD_NUMBER: _ClassVar[int]
CHECK__APP_INSTALLED_FIELD_NUMBER: _ClassVar[int]
PYTHON__GET_ENVIRONMENT_FIELD_NUMBER: _ClassVar[int]
DJANGO__GET_PROJECT_INFO_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
check__health: _check_pb2.HealthResponse
check__django_available: _check_pb2.DjangoAvailableResponse
check__app_installed: _check_pb2.AppInstalledResponse
python__get_environment: _python_pb2.GetEnvironmentResponse
django__get_project_info: _django_pb2.GetProjectInfoResponse
error: Error
def __init__(self, check__health: _Optional[_Union[_check_pb2.HealthResponse, _Mapping]] = ..., check__django_available: _Optional[_Union[_check_pb2.DjangoAvailableResponse, _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: ...
class Error(_message.Message):
__slots__ = ("code", "message", "traceback")
class Code(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
UNKNOWN: _ClassVar[Error.Code]
INVALID_REQUEST: _ClassVar[Error.Code]
PYTHON_ERROR: _ClassVar[Error.Code]
DJANGO_ERROR: _ClassVar[Error.Code]
UNKNOWN: Error.Code
INVALID_REQUEST: Error.Code
PYTHON_ERROR: Error.Code
DJANGO_ERROR: Error.Code
CODE_FIELD_NUMBER: _ClassVar[int]
MESSAGE_FIELD_NUMBER: _ClassVar[int]
TRACEBACK_FIELD_NUMBER: _ClassVar[int]
code: Error.Code
message: str
traceback: str
def __init__(self, code: _Optional[_Union[Error.Code, str]] = ..., message: _Optional[str] = ..., traceback: _Optional[str] = ...) -> None: ...

View file

@ -0,0 +1,66 @@
# WARNING: This file is generated by protobuf. DO NOT EDIT!
# Any changes made to this file will be overwritten when the protobuf files are regenerated.
# Source: v1/python.proto
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: v1/python.proto
# Protobuf Python Version: 5.29.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
29,
1,
'',
'v1/python.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fv1/python.proto\x12\x0e\x64jls.v1.python\"\x9c\x01\n\x06Python\x12\x1e\n\x02os\x18\x01 \x01(\x0b\x32\x12.djls.v1.python.Os\x12\"\n\x04site\x18\x02 \x01(\x0b\x32\x14.djls.v1.python.Site\x12 \n\x03sys\x18\x03 \x01(\x0b\x32\x13.djls.v1.python.Sys\x12,\n\tsysconfig\x18\x04 \x01(\x0b\x32\x19.djls.v1.python.Sysconfig\"f\n\x02Os\x12\x30\n\x07\x65nviron\x18\x01 \x03(\x0b\x32\x1f.djls.v1.python.Os.EnvironEntry\x1a.\n\x0c\x45nvironEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x86\x01\n\x04Site\x12\x34\n\x08packages\x18\x01 \x03(\x0b\x32\".djls.v1.python.Site.PackagesEntry\x1aH\n\rPackagesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12&\n\x05value\x18\x02 \x01(\x0b\x32\x17.djls.v1.python.Package:\x02\x38\x01\"\xe0\x02\n\x03Sys\x12\x13\n\x0b\x64\x65\x62ug_build\x18\x01 \x01(\x08\x12\x10\n\x08\x64\x65v_mode\x18\x02 \x01(\x08\x12\x0f\n\x07is_venv\x18\x03 \x01(\x08\x12\x10\n\x08\x61\x62iflags\x18\x04 \x01(\t\x12\x13\n\x0b\x62\x61se_prefix\x18\x05 \x01(\t\x12\x18\n\x10\x64\x65\x66\x61ult_encoding\x18\x06 \x01(\t\x12\x12\n\nexecutable\x18\x07 \x01(\t\x12\x1b\n\x13\x66ilesystem_encoding\x18\x08 \x01(\t\x12\x1b\n\x13implementation_name\x18\t \x01(\t\x12\x10\n\x08platform\x18\n \x01(\t\x12\x0e\n\x06prefix\x18\x0b \x01(\t\x12\x1c\n\x14\x62uiltin_module_names\x18\x0c \x03(\t\x12\x11\n\tdll_paths\x18\r \x03(\t\x12\x0c\n\x04path\x18\x0e \x03(\t\x12\x31\n\x0cversion_info\x18\x0f \x01(\x0b\x32\x1b.djls.v1.python.VersionInfo\"~\n\x0bVersionInfo\x12\r\n\x05major\x18\x01 \x01(\r\x12\r\n\x05minor\x18\x02 \x01(\r\x12\r\n\x05micro\x18\x03 \x01(\r\x12\x32\n\x0creleaselevel\x18\x04 \x01(\x0e\x32\x1c.djls.v1.python.ReleaseLevel\x12\x0e\n\x06serial\x18\x05 \x01(\r\"\x96\x01\n\tSysconfig\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t\x12\x0f\n\x07include\x18\x02 \x01(\t\x12\x13\n\x0bplatinclude\x18\x03 \x01(\t\x12\x0f\n\x07platlib\x18\x04 \x01(\t\x12\x12\n\nplatstdlib\x18\x05 \x01(\t\x12\x0f\n\x07purelib\x18\x06 \x01(\t\x12\x0f\n\x07scripts\x18\x07 \x01(\t\x12\x0e\n\x06stdlib\x18\x08 \x01(\t\"\x97\x02\n\x07Package\x12\x11\n\tdist_name\x18\x01 \x01(\t\x12\x14\n\x0c\x64ist_version\x18\x02 \x01(\t\x12\x1a\n\rdist_editable\x18\x03 \x01(\x08H\x00\x88\x01\x01\x12\x1e\n\x11\x64ist_entry_points\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x1a\n\rdist_location\x18\x05 \x01(\tH\x02\x88\x01\x01\x12\x15\n\rdist_requires\x18\x06 \x03(\t\x12!\n\x14\x64ist_requires_python\x18\x07 \x01(\tH\x03\x88\x01\x01\x42\x10\n\x0e_dist_editableB\x14\n\x12_dist_entry_pointsB\x10\n\x0e_dist_locationB\x17\n\x15_dist_requires_python\"\x17\n\x15GetEnvironmentRequest\"@\n\x16GetEnvironmentResponse\x12&\n\x06python\x18\x01 \x01(\x0b\x32\x16.djls.v1.python.Python*=\n\x0cReleaseLevel\x12\t\n\x05\x41LPHA\x10\x00\x12\x08\n\x04\x42\x45TA\x10\x01\x12\r\n\tCANDIDATE\x10\x02\x12\t\n\x05\x46INAL\x10\x03\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'v1.python_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_OS_ENVIRONENTRY']._loaded_options = None
_globals['_OS_ENVIRONENTRY']._serialized_options = b'8\001'
_globals['_SITE_PACKAGESENTRY']._loaded_options = None
_globals['_SITE_PACKAGESENTRY']._serialized_options = b'8\001'
_globals['_RELEASELEVEL']._serialized_start=1444
_globals['_RELEASELEVEL']._serialized_end=1505
_globals['_PYTHON']._serialized_start=36
_globals['_PYTHON']._serialized_end=192
_globals['_OS']._serialized_start=194
_globals['_OS']._serialized_end=296
_globals['_OS_ENVIRONENTRY']._serialized_start=250
_globals['_OS_ENVIRONENTRY']._serialized_end=296
_globals['_SITE']._serialized_start=299
_globals['_SITE']._serialized_end=433
_globals['_SITE_PACKAGESENTRY']._serialized_start=361
_globals['_SITE_PACKAGESENTRY']._serialized_end=433
_globals['_SYS']._serialized_start=436
_globals['_SYS']._serialized_end=788
_globals['_VERSIONINFO']._serialized_start=790
_globals['_VERSIONINFO']._serialized_end=916
_globals['_SYSCONFIG']._serialized_start=919
_globals['_SYSCONFIG']._serialized_end=1069
_globals['_PACKAGE']._serialized_start=1072
_globals['_PACKAGE']._serialized_end=1351
_globals['_GETENVIRONMENTREQUEST']._serialized_start=1353
_globals['_GETENVIRONMENTREQUEST']._serialized_end=1376
_globals['_GETENVIRONMENTRESPONSE']._serialized_start=1378
_globals['_GETENVIRONMENTRESPONSE']._serialized_end=1442
# @@protoc_insertion_point(module_scope)

View file

@ -0,0 +1,156 @@
# WARNING: This file is generated by protobuf. DO NOT EDIT!
# Any changes made to this file will be overwritten when the protobuf files are regenerated.
# Source: v1/python.proto
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class ReleaseLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
ALPHA: _ClassVar[ReleaseLevel]
BETA: _ClassVar[ReleaseLevel]
CANDIDATE: _ClassVar[ReleaseLevel]
FINAL: _ClassVar[ReleaseLevel]
ALPHA: ReleaseLevel
BETA: ReleaseLevel
CANDIDATE: ReleaseLevel
FINAL: ReleaseLevel
class Python(_message.Message):
__slots__ = ("os", "site", "sys", "sysconfig")
OS_FIELD_NUMBER: _ClassVar[int]
SITE_FIELD_NUMBER: _ClassVar[int]
SYS_FIELD_NUMBER: _ClassVar[int]
SYSCONFIG_FIELD_NUMBER: _ClassVar[int]
os: Os
site: Site
sys: Sys
sysconfig: Sysconfig
def __init__(self, os: _Optional[_Union[Os, _Mapping]] = ..., site: _Optional[_Union[Site, _Mapping]] = ..., sys: _Optional[_Union[Sys, _Mapping]] = ..., sysconfig: _Optional[_Union[Sysconfig, _Mapping]] = ...) -> None: ...
class Os(_message.Message):
__slots__ = ("environ",)
class EnvironEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: str
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
ENVIRON_FIELD_NUMBER: _ClassVar[int]
environ: _containers.ScalarMap[str, str]
def __init__(self, environ: _Optional[_Mapping[str, str]] = ...) -> None: ...
class Site(_message.Message):
__slots__ = ("packages",)
class PackagesEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: Package
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[Package, _Mapping]] = ...) -> None: ...
PACKAGES_FIELD_NUMBER: _ClassVar[int]
packages: _containers.MessageMap[str, Package]
def __init__(self, packages: _Optional[_Mapping[str, Package]] = ...) -> None: ...
class Sys(_message.Message):
__slots__ = ("debug_build", "dev_mode", "is_venv", "abiflags", "base_prefix", "default_encoding", "executable", "filesystem_encoding", "implementation_name", "platform", "prefix", "builtin_module_names", "dll_paths", "path", "version_info")
DEBUG_BUILD_FIELD_NUMBER: _ClassVar[int]
DEV_MODE_FIELD_NUMBER: _ClassVar[int]
IS_VENV_FIELD_NUMBER: _ClassVar[int]
ABIFLAGS_FIELD_NUMBER: _ClassVar[int]
BASE_PREFIX_FIELD_NUMBER: _ClassVar[int]
DEFAULT_ENCODING_FIELD_NUMBER: _ClassVar[int]
EXECUTABLE_FIELD_NUMBER: _ClassVar[int]
FILESYSTEM_ENCODING_FIELD_NUMBER: _ClassVar[int]
IMPLEMENTATION_NAME_FIELD_NUMBER: _ClassVar[int]
PLATFORM_FIELD_NUMBER: _ClassVar[int]
PREFIX_FIELD_NUMBER: _ClassVar[int]
BUILTIN_MODULE_NAMES_FIELD_NUMBER: _ClassVar[int]
DLL_PATHS_FIELD_NUMBER: _ClassVar[int]
PATH_FIELD_NUMBER: _ClassVar[int]
VERSION_INFO_FIELD_NUMBER: _ClassVar[int]
debug_build: bool
dev_mode: bool
is_venv: bool
abiflags: str
base_prefix: str
default_encoding: str
executable: str
filesystem_encoding: str
implementation_name: str
platform: str
prefix: str
builtin_module_names: _containers.RepeatedScalarFieldContainer[str]
dll_paths: _containers.RepeatedScalarFieldContainer[str]
path: _containers.RepeatedScalarFieldContainer[str]
version_info: VersionInfo
def __init__(self, debug_build: bool = ..., dev_mode: bool = ..., is_venv: bool = ..., abiflags: _Optional[str] = ..., base_prefix: _Optional[str] = ..., default_encoding: _Optional[str] = ..., executable: _Optional[str] = ..., filesystem_encoding: _Optional[str] = ..., implementation_name: _Optional[str] = ..., platform: _Optional[str] = ..., prefix: _Optional[str] = ..., builtin_module_names: _Optional[_Iterable[str]] = ..., dll_paths: _Optional[_Iterable[str]] = ..., path: _Optional[_Iterable[str]] = ..., version_info: _Optional[_Union[VersionInfo, _Mapping]] = ...) -> None: ...
class VersionInfo(_message.Message):
__slots__ = ("major", "minor", "micro", "releaselevel", "serial")
MAJOR_FIELD_NUMBER: _ClassVar[int]
MINOR_FIELD_NUMBER: _ClassVar[int]
MICRO_FIELD_NUMBER: _ClassVar[int]
RELEASELEVEL_FIELD_NUMBER: _ClassVar[int]
SERIAL_FIELD_NUMBER: _ClassVar[int]
major: int
minor: int
micro: int
releaselevel: ReleaseLevel
serial: int
def __init__(self, major: _Optional[int] = ..., minor: _Optional[int] = ..., micro: _Optional[int] = ..., releaselevel: _Optional[_Union[ReleaseLevel, str]] = ..., serial: _Optional[int] = ...) -> None: ...
class Sysconfig(_message.Message):
__slots__ = ("data", "include", "platinclude", "platlib", "platstdlib", "purelib", "scripts", "stdlib")
DATA_FIELD_NUMBER: _ClassVar[int]
INCLUDE_FIELD_NUMBER: _ClassVar[int]
PLATINCLUDE_FIELD_NUMBER: _ClassVar[int]
PLATLIB_FIELD_NUMBER: _ClassVar[int]
PLATSTDLIB_FIELD_NUMBER: _ClassVar[int]
PURELIB_FIELD_NUMBER: _ClassVar[int]
SCRIPTS_FIELD_NUMBER: _ClassVar[int]
STDLIB_FIELD_NUMBER: _ClassVar[int]
data: str
include: str
platinclude: str
platlib: str
platstdlib: str
purelib: str
scripts: str
stdlib: str
def __init__(self, data: _Optional[str] = ..., include: _Optional[str] = ..., platinclude: _Optional[str] = ..., platlib: _Optional[str] = ..., platstdlib: _Optional[str] = ..., purelib: _Optional[str] = ..., scripts: _Optional[str] = ..., stdlib: _Optional[str] = ...) -> None: ...
class Package(_message.Message):
__slots__ = ("dist_name", "dist_version", "dist_editable", "dist_entry_points", "dist_location", "dist_requires", "dist_requires_python")
DIST_NAME_FIELD_NUMBER: _ClassVar[int]
DIST_VERSION_FIELD_NUMBER: _ClassVar[int]
DIST_EDITABLE_FIELD_NUMBER: _ClassVar[int]
DIST_ENTRY_POINTS_FIELD_NUMBER: _ClassVar[int]
DIST_LOCATION_FIELD_NUMBER: _ClassVar[int]
DIST_REQUIRES_FIELD_NUMBER: _ClassVar[int]
DIST_REQUIRES_PYTHON_FIELD_NUMBER: _ClassVar[int]
dist_name: str
dist_version: str
dist_editable: bool
dist_entry_points: str
dist_location: str
dist_requires: _containers.RepeatedScalarFieldContainer[str]
dist_requires_python: str
def __init__(self, dist_name: _Optional[str] = ..., dist_version: _Optional[str] = ..., dist_editable: bool = ..., dist_entry_points: _Optional[str] = ..., dist_location: _Optional[str] = ..., dist_requires: _Optional[_Iterable[str]] = ..., dist_requires_python: _Optional[str] = ...) -> None: ...
class GetEnvironmentRequest(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class GetEnvironmentResponse(_message.Message):
__slots__ = ("python",)
PYTHON_FIELD_NUMBER: _ClassVar[int]
python: Python
def __init__(self, python: _Optional[_Union[Python, _Mapping]] = ...) -> None: ...