mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-07-24 12:53:52 +00:00
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:
parent
643a47953e
commit
0a6e975ca5
38 changed files with 1484 additions and 685 deletions
|
@ -9,7 +9,7 @@ trim_trailing_whitespace = true
|
||||||
[{,.}{j,J}ustfile]
|
[{,.}{j,J}ustfile]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
[*.{py,rst,ini,md}]
|
[*.{just,proto,py,rst,ini,md}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
[*.py]
|
[*.py]
|
||||||
|
|
|
@ -22,16 +22,84 @@ fmt:
|
||||||
# Generate protobuf code for both Rust and Python
|
# Generate protobuf code for both Rust and Python
|
||||||
[no-cd]
|
[no-cd]
|
||||||
gen:
|
gen:
|
||||||
@just proto rust
|
@just proto rust
|
||||||
@just proto py
|
@just proto py
|
||||||
|
|
||||||
# Generate protobuf code for Rust
|
# Generate protobuf code for Rust
|
||||||
[no-cd]
|
[no-cd]
|
||||||
rust: check
|
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
|
# Generate protobuf code for Python
|
||||||
[no-cd]
|
[no-cd]
|
||||||
py: check
|
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)
|
||||||
|
|
|
@ -9,7 +9,6 @@ djls-django = { path = "crates/djls-django" }
|
||||||
djls-ipc = { path = "crates/djls-ipc" }
|
djls-ipc = { path = "crates/djls-ipc" }
|
||||||
djls-python = { path = "crates/djls-python" }
|
djls-python = { path = "crates/djls-python" }
|
||||||
djls-server = { path = "crates/djls-server" }
|
djls-server = { path = "crates/djls-server" }
|
||||||
djls-types = { path = "crates/djls-types" }
|
|
||||||
djls-worker = { path = "crates/djls-worker" }
|
djls-worker = { path = "crates/djls-worker" }
|
||||||
|
|
||||||
anyhow = "1.0.94"
|
anyhow = "1.0.94"
|
||||||
|
|
3
Justfile
3
Justfile
|
@ -8,6 +8,9 @@ mod proto ".just/proto.just"
|
||||||
default:
|
default:
|
||||||
@just --list
|
@just --list
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf target/
|
||||||
|
|
||||||
# run pre-commit on all files
|
# run pre-commit on all files
|
||||||
lint:
|
lint:
|
||||||
@just --fmt
|
@just --fmt
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use clap::{Args, Parser, Subcommand};
|
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;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
|
@ -41,8 +43,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Serve(opts) => {
|
Commands::Serve(opts) => {
|
||||||
let python =
|
println!("Starting LSP server...");
|
||||||
PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?;
|
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?
|
djls_server::serve(python).await?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use djls_ipc::{JsonResponse, PythonProcess, TransportError, TransportMessage, TransportResponse};
|
use djls_ipc::v1::*;
|
||||||
use serde::Deserialize;
|
use djls_ipc::{ProcessError, PythonProcess};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -20,23 +20,6 @@ impl fmt::Display for App {
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Apps(Vec<App>);
|
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 {
|
impl Apps {
|
||||||
pub fn from_strings(apps: Vec<String>) -> Self {
|
pub fn from_strings(apps: Vec<String>) -> Self {
|
||||||
Self(apps.into_iter().map(App).collect())
|
Self(apps.into_iter().map(App).collect())
|
||||||
|
@ -54,18 +37,21 @@ impl Apps {
|
||||||
self.apps().iter()
|
self.apps().iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result<bool, TransportError> {
|
pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result<bool, ProcessError> {
|
||||||
let message = TransportMessage::Json("installed_apps_check".to_string());
|
let request = messages::Request {
|
||||||
let response = python.send(message, Some(vec![app.to_string()]))?;
|
command: Some(messages::request::Command::CheckAppInstalled(
|
||||||
match response {
|
check::AppInstalledRequest {
|
||||||
TransportResponse::Json(json_str) => {
|
app_name: app.to_string(),
|
||||||
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(),
|
|
||||||
)),
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +1,26 @@
|
||||||
use crate::apps::Apps;
|
|
||||||
use crate::gis::{check_gis_setup, GISError};
|
use crate::gis::{check_gis_setup, GISError};
|
||||||
use crate::templates::TemplateTags;
|
use djls_ipc::v1::*;
|
||||||
use djls_ipc::{JsonResponse, PythonProcess, TransportError, TransportMessage, TransportResponse};
|
use djls_ipc::{ProcessError, PythonProcess, TransportError};
|
||||||
use djls_python::{ImportCheck, Python};
|
use djls_python::{ImportCheck, Python};
|
||||||
use serde::Deserialize;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DjangoProject {
|
pub struct DjangoProject {
|
||||||
py: Python,
|
py: Python,
|
||||||
python: PythonProcess,
|
python: PythonProcess,
|
||||||
settings_module: String,
|
version: 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(),
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DjangoProject {
|
impl DjangoProject {
|
||||||
fn new(
|
fn new(py: Python, python: PythonProcess, version: String) -> Self {
|
||||||
py: Python,
|
|
||||||
python: PythonProcess,
|
|
||||||
settings_module: String,
|
|
||||||
installed_apps: Apps,
|
|
||||||
templatetags: TemplateTags,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
py,
|
py,
|
||||||
python,
|
python,
|
||||||
settings_module,
|
version,
|
||||||
installed_apps,
|
|
||||||
templatetags,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup(mut python: PythonProcess) -> Result<Self, ProjectError> {
|
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 py = Python::setup(&mut python)?;
|
||||||
|
|
||||||
let has_django = ImportCheck::check(&mut python, Some(vec!["django".to_string()]))?;
|
let has_django = ImportCheck::check(&mut python, Some(vec!["django".to_string()]))?;
|
||||||
|
@ -74,45 +37,52 @@ impl DjangoProject {
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
py,
|
py,
|
||||||
python,
|
python,
|
||||||
settings_module,
|
version: String::new(),
|
||||||
installed_apps: Apps::default(),
|
|
||||||
templatetags: TemplateTags::default(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = DjangoSetup::setup(&mut python)?;
|
let request = messages::Request {
|
||||||
let setup: DjangoSetup = response
|
command: Some(messages::request::Command::DjangoGetProjectInfo(
|
||||||
.data()
|
django::GetProjectInfoRequest {},
|
||||||
.clone()
|
)),
|
||||||
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
|
};
|
||||||
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))?;
|
|
||||||
|
|
||||||
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,
|
py,
|
||||||
python,
|
python,
|
||||||
settings_module,
|
version,
|
||||||
Apps::from_strings(setup.installed_apps.to_vec()),
|
})
|
||||||
setup.templatetags,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn py(&self) -> &Python {
|
pub fn py(&self) -> &Python {
|
||||||
&self.py
|
&self.py
|
||||||
}
|
}
|
||||||
|
|
||||||
fn settings_module(&self) -> &String {
|
fn version(&self) -> &String {
|
||||||
&self.settings_module
|
&self.version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DjangoProject {
|
impl fmt::Display for DjangoProject {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(f, "Django Project")?;
|
writeln!(f, "Django Project")?;
|
||||||
writeln!(f, "Settings Module: {}", self.settings_module)?;
|
writeln!(f, "Version: {}", self.version)?;
|
||||||
writeln!(f, "Installed Apps:")?;
|
|
||||||
write!(f, "{}", self.installed_apps)?;
|
|
||||||
writeln!(f, "Template Tags:")?;
|
|
||||||
write!(f, "{}", self.templatetags)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,22 +91,18 @@ impl fmt::Display for DjangoProject {
|
||||||
pub enum ProjectError {
|
pub enum ProjectError {
|
||||||
#[error("Django is not installed or cannot be imported")]
|
#[error("Django is not installed or cannot be imported")]
|
||||||
DjangoNotFound,
|
DjangoNotFound,
|
||||||
|
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("GIS error: {0}")]
|
#[error("GIS error: {0}")]
|
||||||
Gis(#[from] GISError),
|
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)]
|
||||||
Packaging(#[from] djls_python::PackagingError),
|
Packaging(#[from] djls_python::PackagingError),
|
||||||
|
#[error("Process error: {0}")]
|
||||||
|
Process(#[from] ProcessError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Python(#[from] djls_python::PythonError),
|
Python(#[from] djls_python::PythonError),
|
||||||
|
|
||||||
#[error("Transport error: {0}")]
|
#[error("Transport error: {0}")]
|
||||||
Transport(#[from] TransportError),
|
Transport(#[from] TransportError),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::apps::Apps;
|
use crate::apps::Apps;
|
||||||
use djls_ipc::{PythonProcess, TransportError};
|
use djls_ipc::{ProcessError, PythonProcess, TransportError};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
pub fn check_gis_setup(python: &mut PythonProcess) -> Result<bool, GISError> {
|
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 {
|
pub enum GISError {
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("JSON parsing error: {0}")]
|
#[error("JSON parsing error: {0}")]
|
||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
|
#[error("Process error: {0}")]
|
||||||
|
Process(#[from] ProcessError),
|
||||||
#[error("Transport error: {0}")]
|
#[error("Transport error: {0}")]
|
||||||
Transport(#[from] TransportError),
|
Transport(#[from] TransportError),
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,16 @@ version = "0.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
djls-types = { workspace = true }
|
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
prost = { workspace = true }
|
|
||||||
bytes = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { 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
38
crates/djls-ipc/build.rs
Normal 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();
|
||||||
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
mod process;
|
mod process;
|
||||||
|
mod proto;
|
||||||
mod transport;
|
mod transport;
|
||||||
|
|
||||||
|
pub use process::ProcessError;
|
||||||
pub use process::PythonProcess;
|
pub use process::PythonProcess;
|
||||||
pub use transport::parse_json_response;
|
pub use proto::v1;
|
||||||
pub use transport::parse_raw_response;
|
|
||||||
pub use transport::JsonResponse;
|
|
||||||
pub use transport::Transport;
|
pub use transport::Transport;
|
||||||
pub use transport::TransportError;
|
pub use transport::TransportError;
|
||||||
pub use transport::TransportMessage;
|
|
||||||
pub use transport::TransportResponse;
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::transport::{
|
use crate::proto::v1::*;
|
||||||
Transport, TransportError, TransportMessage, TransportProtocol, TransportResponse,
|
use crate::transport::{Transport, TransportError};
|
||||||
};
|
use std::ffi::OsStr;
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
@ -9,75 +9,130 @@ use tokio::time;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PythonProcess {
|
pub struct PythonProcess {
|
||||||
transport: Arc<Mutex<Box<dyn TransportProtocol>>>,
|
transport: Arc<Mutex<Transport>>,
|
||||||
_child: Child,
|
_child: Child,
|
||||||
healthy: Arc<AtomicBool>,
|
healthy: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PythonProcess {
|
impl PythonProcess {
|
||||||
pub fn new(
|
pub fn new<I, S>(
|
||||||
module: &str,
|
module: &str,
|
||||||
transport: Transport,
|
args: Option<I>,
|
||||||
health_check_interval: Option<Duration>,
|
health_check_interval: Option<Duration>,
|
||||||
) -> Result<Self, TransportError> {
|
) -> Result<Self, ProcessError>
|
||||||
let mut child = Command::new("python")
|
where
|
||||||
.arg("-m")
|
I: IntoIterator<Item = S>,
|
||||||
.arg(module)
|
S: AsRef<OsStr>,
|
||||||
.stdin(Stdio::piped())
|
{
|
||||||
.stdout(Stdio::piped())
|
let mut command = Command::new("python");
|
||||||
.spawn()?;
|
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 stdin = child.stdin.take().unwrap();
|
||||||
let stdout = child.stdout.take().unwrap();
|
let stdout = child.stdout.take().unwrap();
|
||||||
|
|
||||||
|
let transport = Transport::new(stdin, stdout)?;
|
||||||
|
|
||||||
let process = Self {
|
let process = Self {
|
||||||
transport: Arc::new(Mutex::new(transport.create(stdin, stdout)?)),
|
transport: Arc::new(Mutex::new(transport)),
|
||||||
_child: child,
|
_child: child,
|
||||||
healthy: Arc::new(AtomicBool::new(true)),
|
healthy: Arc::new(AtomicBool::new(true)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(interval) = health_check_interval {
|
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)
|
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 {
|
pub fn is_healthy(&self) -> bool {
|
||||||
self.healthy.load(Ordering::SeqCst)
|
self.healthy.load(Ordering::SeqCst)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(
|
pub fn send(
|
||||||
&mut self,
|
&mut self,
|
||||||
message: TransportMessage,
|
request: messages::Request,
|
||||||
args: Option<Vec<String>>,
|
) -> Result<messages::Response, TransportError> {
|
||||||
) -> Result<TransportResponse, TransportError> {
|
|
||||||
let mut transport = self.transport.lock().unwrap();
|
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),
|
||||||
|
}
|
||||||
|
|
17
crates/djls-ipc/src/proto.rs
Normal file
17
crates/djls-ipc/src/proto.rs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,320 +1,59 @@
|
||||||
use djls_types::proto::*;
|
use crate::process::ProcessError;
|
||||||
|
use crate::proto::v1::*;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use serde::{Deserialize, Serialize};
|
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
|
||||||
use serde_json::Value;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::io::{BufRead, BufReader, BufWriter, Write};
|
|
||||||
use std::process::{ChildStdin, ChildStdout};
|
use std::process::{ChildStdin, ChildStdout};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum TransportError {
|
pub struct Transport {
|
||||||
#[error("IO error: {0}")]
|
reader: Arc<Mutex<BufReader<ChildStdout>>>,
|
||||||
Io(#[from] std::io::Error),
|
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
|
||||||
#[error("JSON error: {0}")]
|
|
||||||
Json(#[from] serde_json::Error),
|
|
||||||
#[error("Process error: {0}")]
|
|
||||||
Process(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Transport {
|
|
||||||
Raw,
|
|
||||||
Json,
|
|
||||||
Protobuf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Transport {
|
impl Transport {
|
||||||
pub fn create(
|
pub fn new(mut stdin: ChildStdin, mut stdout: ChildStdout) -> Result<Self, ProcessError> {
|
||||||
&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)?;
|
|
||||||
stdin.flush().map_err(TransportError::Io)?;
|
stdin.flush().map_err(TransportError::Io)?;
|
||||||
|
|
||||||
let mut ready_line = String::new();
|
let mut ready_line = String::new();
|
||||||
BufReader::new(&mut stdout)
|
BufReader::new(&mut stdout)
|
||||||
.read_line(&mut ready_line)
|
.read_line(&mut ready_line)
|
||||||
.map_err(TransportError::Io)?;
|
.map_err(TransportError::Io)?;
|
||||||
|
|
||||||
if ready_line.trim() != "ready" {
|
if ready_line.trim() != "ready" {
|
||||||
return Err(TransportError::Process(
|
return Err(ProcessError::Ready("Python process not ready".to_string()));
|
||||||
"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 {
|
Ok(Self {
|
||||||
reader: Arc::new(Mutex::new(BufReader::new(stdout))),
|
reader: Arc::new(Mutex::new(BufReader::new(stdout))),
|
||||||
writer: Arc::new(Mutex::new(BufWriter::new(stdin))),
|
writer: Arc::new(Mutex::new(BufWriter::new(stdin))),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn health_check(&mut self) -> Result<(), TransportError> {
|
pub fn send(
|
||||||
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(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
message: TransportMessage,
|
message: messages::Request,
|
||||||
args: Option<Vec<String>>,
|
) -> Result<messages::Response, TransportError> {
|
||||||
) -> Result<TransportResponse, TransportError> {
|
let buf = message.encode_to_vec();
|
||||||
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(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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)?;
|
writer.flush().map_err(TransportError::Io)?;
|
||||||
|
|
||||||
let mut reader = self.reader.lock().unwrap();
|
let mut reader = self.reader.lock().map_err(|_| {
|
||||||
let mut line = String::new();
|
TransportError::Io(std::io::Error::new(
|
||||||
reader.read_line(&mut line).map_err(TransportError::Io)?;
|
std::io::ErrorKind::Other,
|
||||||
Ok(TransportResponse::Raw(line.trim().to_string()))
|
"Failed to acquire reader lock",
|
||||||
}
|
))
|
||||||
}
|
})?;
|
||||||
|
|
||||||
#[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 length_bytes = [0u8; 4];
|
let mut length_bytes = [0u8; 4];
|
||||||
reader
|
reader
|
||||||
.read_exact(&mut length_bytes)
|
.read_exact(&mut length_bytes)
|
||||||
|
@ -326,17 +65,17 @@ impl TransportProtocol for ProtobufTransport {
|
||||||
.read_exact(&mut message_bytes)
|
.read_exact(&mut message_bytes)
|
||||||
.map_err(TransportError::Io)?;
|
.map_err(TransportError::Io)?;
|
||||||
|
|
||||||
let response = FromAgent::decode(message_bytes.as_slice())
|
messages::Response::decode(message_bytes.as_slice())
|
||||||
.map_err(|e| TransportError::Process(e.to_string()))?;
|
.map_err(|e| TransportError::Decode(e.to_string()))
|
||||||
|
|
||||||
Ok(TransportResponse::Protobuf(response))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_raw_response(response: String) -> Result<String, TransportError> {
|
#[derive(thiserror::Error, Debug)]
|
||||||
Ok(response)
|
pub enum TransportError {
|
||||||
}
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
pub fn parse_json_response(response: String) -> Result<JsonResponse, TransportError> {
|
#[error("Task error: {0}")]
|
||||||
serde_json::from_str(&response).map_err(TransportError::Json)
|
Task(#[from] tokio::task::JoinError),
|
||||||
|
#[error("Failed to decode message: {0}")]
|
||||||
|
Decode(String),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
@ -6,15 +7,25 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct Package {
|
pub struct Package {
|
||||||
name: String,
|
dist_name: String,
|
||||||
version: String,
|
dist_version: String,
|
||||||
location: Option<PathBuf>,
|
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 {
|
impl fmt::Display for Package {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{} {}", self.name, self.version)?;
|
write!(f, "{} {}", self.dist_name, self.dist_version)?;
|
||||||
if let Some(location) = &self.location {
|
if let Some(location) = &self.dist_location {
|
||||||
write!(f, " ({})", location.display())?;
|
write!(f, " ({})", location.display())?;
|
||||||
}
|
}
|
||||||
Ok(())
|
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 {
|
impl FromIterator<(String, Package)> for Packages {
|
||||||
fn from_iter<T: IntoIterator<Item = (String, Package)>>(iter: T) -> Self {
|
fn from_iter<T: IntoIterator<Item = (String, Package)>>(iter: T) -> Self {
|
||||||
Self(HashMap::from_iter(iter))
|
Self(HashMap::from_iter(iter))
|
||||||
|
@ -39,7 +56,7 @@ impl FromIterator<(String, Package)> for Packages {
|
||||||
impl fmt::Display for Packages {
|
impl fmt::Display for Packages {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let mut packages: Vec<_> = self.packages();
|
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() {
|
if packages.is_empty() {
|
||||||
writeln!(f, " (no packages installed)")?;
|
writeln!(f, " (no packages installed)")?;
|
||||||
|
@ -57,18 +74,6 @@ pub struct ImportCheck {
|
||||||
can_import: bool,
|
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 {
|
impl ImportCheck {
|
||||||
pub fn can_import(&self) -> bool {
|
pub fn can_import(&self) -> bool {
|
||||||
self.can_import
|
self.can_import
|
||||||
|
@ -76,19 +81,24 @@ impl ImportCheck {
|
||||||
|
|
||||||
pub fn check(
|
pub fn check(
|
||||||
python: &mut PythonProcess,
|
python: &mut PythonProcess,
|
||||||
modules: Option<Vec<String>>,
|
_modules: Option<Vec<String>>,
|
||||||
) -> Result<bool, PackagingError> {
|
) -> Result<bool, PackagingError> {
|
||||||
let message = TransportMessage::Json("has_import".to_string());
|
let request = messages::Request {
|
||||||
let response = python.send(message, modules)?;
|
command: Some(messages::request::Command::CheckDjangoAvailable(
|
||||||
match response {
|
check::DjangoAvailableRequest {},
|
||||||
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 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(
|
_ => Err(PackagingError::Process(ProcessError::Response)),
|
||||||
"Unexpected response type".to_string(),
|
|
||||||
))),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,6 +113,8 @@ pub enum PackagingError {
|
||||||
|
|
||||||
#[error("Transport error: {0}")]
|
#[error("Transport error: {0}")]
|
||||||
Transport(#[from] TransportError),
|
Transport(#[from] TransportError),
|
||||||
|
#[error("Process error: {0}")]
|
||||||
|
Process(#[from] ProcessError),
|
||||||
|
|
||||||
#[error("UTF-8 conversion error: {0}")]
|
#[error("UTF-8 conversion error: {0}")]
|
||||||
Utf8(#[from] std::string::FromUtf8Error),
|
Utf8(#[from] std::string::FromUtf8Error),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::packaging::{Packages, PackagingError};
|
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 serde::Deserialize;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -8,16 +9,45 @@ use std::path::PathBuf;
|
||||||
pub struct VersionInfo {
|
pub struct VersionInfo {
|
||||||
major: u8,
|
major: u8,
|
||||||
minor: u8,
|
minor: u8,
|
||||||
patch: u8,
|
micro: u8,
|
||||||
suffix: Option<String>,
|
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 {
|
impl fmt::Display for VersionInfo {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
|
write!(f, "{}.{}.{}", self.major, self.minor, self.micro)?;
|
||||||
if let Some(suffix) = &self.suffix {
|
|
||||||
write!(f, "{}", suffix)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,30 +88,52 @@ pub struct Python {
|
||||||
packages: Packages,
|
packages: Packages,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<JsonResponse> for Python {
|
impl Python {
|
||||||
type Error = TransportError;
|
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> {
|
let response = python.send(request).map_err(PythonError::Transport)?;
|
||||||
response
|
|
||||||
.data()
|
match response.result {
|
||||||
.clone()
|
Some(messages::response::Result::PythonGetEnvironment(response)) => response
|
||||||
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
|
.python
|
||||||
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))
|
.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 {
|
impl From<python::Python> for Python {
|
||||||
pub fn setup(python: &mut PythonProcess) -> Result<Self, PythonError> {
|
fn from(p: python::Python) -> Self {
|
||||||
let message = TransportMessage::Json("python_setup".to_string());
|
let sys = p.sys.unwrap();
|
||||||
let response = python.send(message, None)?;
|
let sysconfig = p.sysconfig.unwrap();
|
||||||
match response {
|
let site = p.site.unwrap();
|
||||||
TransportResponse::Json(json_str) => {
|
|
||||||
let json_response: JsonResponse = serde_json::from_str(&json_str)?;
|
Self {
|
||||||
Ok(Self::try_from(json_response)?)
|
version_info: sys.version_info.unwrap_or_default().into(),
|
||||||
}
|
sysconfig_paths: SysconfigPaths {
|
||||||
_ => Err(PythonError::Transport(TransportError::Process(
|
data: PathBuf::from(sysconfig.data),
|
||||||
"Unexpected response type".to_string(),
|
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 {
|
pub enum PythonError {
|
||||||
#[error("Python execution failed: {0}")]
|
#[error("Python execution failed: {0}")]
|
||||||
Execution(String),
|
Execution(String),
|
||||||
|
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("JSON parsing error: {0}")]
|
#[error("JSON parsing error: {0}")]
|
||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
|
|
||||||
#[error("Packaging error: {0}")]
|
#[error("Packaging error: {0}")]
|
||||||
Packaging(#[from] PackagingError),
|
Packaging(#[from] PackagingError),
|
||||||
|
|
||||||
#[error("Integer parsing error: {0}")]
|
#[error("Integer parsing error: {0}")]
|
||||||
Parse(#[from] std::num::ParseIntError),
|
Parse(#[from] std::num::ParseIntError),
|
||||||
|
#[error("Process error: {0}")]
|
||||||
|
Process(#[from] ProcessError),
|
||||||
#[error("Failed to locate Python executable: {0}")]
|
#[error("Failed to locate Python executable: {0}")]
|
||||||
PythonNotFound(#[from] which::Error),
|
PythonNotFound(#[from] which::Error),
|
||||||
|
|
||||||
#[error("Transport error: {0}")]
|
#[error("Transport error: {0}")]
|
||||||
Transport(#[from] TransportError),
|
Transport(#[from] TransportError),
|
||||||
|
|
||||||
#[error("UTF-8 conversion error: {0}")]
|
#[error("UTF-8 conversion error: {0}")]
|
||||||
Utf8(#[from] std::string::FromUtf8Error),
|
Utf8(#[from] std::string::FromUtf8Error),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
pub mod proto {
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/djls.rs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
use proto::*;
|
|
|
@ -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
23
proto/v1/check.proto
Normal 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
15
proto/v1/django.proto
Normal 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
40
proto/v1/messages.proto
Normal 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
80
proto/v1/python.proto
Normal 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
12
python/djls/_typing.py
Normal 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
131
python/djls/agent.py
Normal 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
209
python/djls/commands.py
Normal 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,
|
||||||
|
]
|
|
@ -9,34 +9,6 @@ from .scripts import has_import
|
||||||
from .scripts import python_setup
|
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]:
|
def handle_json_command(data: dict[str, Any]) -> dict[str, Any]:
|
||||||
command = data["command"]
|
command = data["command"]
|
||||||
args = data.get("args", []) # Get args if they exist
|
args = data.get("args", []) # Get args if they exist
|
||||||
|
@ -84,20 +56,12 @@ def main():
|
||||||
if not line:
|
if not line:
|
||||||
break
|
break
|
||||||
|
|
||||||
if transport_type == "json":
|
data = json.loads(line)
|
||||||
data = json.loads(line)
|
response = handle_json_command(data)
|
||||||
response = handle_json_command(data)
|
print(json.dumps(response), flush=True)
|
||||||
print(json.dumps(response), flush=True)
|
|
||||||
else:
|
|
||||||
command = line.strip()
|
|
||||||
response = handle_command(command)
|
|
||||||
print(response, flush=True)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if transport_type == "json":
|
print(json.dumps({"status": "error", "error": str(e)}), flush=True)
|
||||||
print(json.dumps({"status": "error", "error": str(e)}), flush=True)
|
|
||||||
else:
|
|
||||||
print(f"error: {str(e)}", flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -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)
|
|
4
python/djls/proto/v1/__init__.py
Normal file
4
python/djls/proto/v1/__init__.py
Normal 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
|
||||||
|
|
50
python/djls/proto/v1/check_pb2.py
Normal file
50
python/djls/proto/v1/check_pb2.py
Normal 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)
|
47
python/djls/proto/v1/check_pb2.pyi
Normal file
47
python/djls/proto/v1/check_pb2.pyi
Normal 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: ...
|
44
python/djls/proto/v1/django_pb2.py
Normal file
44
python/djls/proto/v1/django_pb2.py
Normal 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)
|
25
python/djls/proto/v1/django_pb2.pyi
Normal file
25
python/djls/proto/v1/django_pb2.pyi
Normal 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: ...
|
49
python/djls/proto/v1/messages_pb2.py
Normal file
49
python/djls/proto/v1/messages_pb2.py
Normal 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)
|
63
python/djls/proto/v1/messages_pb2.pyi
Normal file
63
python/djls/proto/v1/messages_pb2.pyi
Normal 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: ...
|
66
python/djls/proto/v1/python_pb2.py
Normal file
66
python/djls/proto/v1/python_pb2.py
Normal 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)
|
156
python/djls/proto/v1/python_pb2.pyi
Normal file
156
python/djls/proto/v1/python_pb2.pyi
Normal 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: ...
|
Loading…
Add table
Add a link
Reference in a new issue