From 0a6e975ca521eccbde3408f1bb25e1a910eaa45a Mon Sep 17 00:00:00 2001 From: Josh Thomas Date: Thu, 12 Dec 2024 16:53:49 -0600 Subject: [PATCH] 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 --- .editorconfig | 2 +- .just/proto.just | 76 +++++- Cargo.toml | 1 - Justfile | 3 + crates/djlc-cli/src/main.rs | 13 +- crates/djls-django/src/apps.rs | 46 ++-- crates/djls-django/src/django.rs | 104 +++----- crates/djls-django/src/gis.rs | 6 +- crates/djls-ipc/Cargo.toml | 11 +- crates/djls-ipc/build.rs | 38 +++ crates/djls-ipc/src/lib.rs | 8 +- crates/djls-ipc/src/process.rs | 143 +++++++---- crates/djls-ipc/src/proto.rs | 17 ++ crates/djls-ipc/src/transport.rs | 341 +++----------------------- crates/djls-python/src/packaging.rs | 72 +++--- crates/djls-python/src/python.rs | 115 ++++++--- crates/djls-types/Cargo.toml | 11 - crates/djls-types/build.rs | 24 -- crates/djls-types/src/lib.rs | 5 - proto/messages.proto | 29 --- proto/v1/check.proto | 23 ++ proto/v1/django.proto | 15 ++ proto/v1/messages.proto | 40 +++ proto/v1/python.proto | 80 ++++++ python/djls/_typing.py | 12 + python/djls/agent.py | 131 ++++++++++ python/djls/commands.py | 209 ++++++++++++++++ python/djls/lsp.py | 44 +--- python/djls/messages_pb2.py | 46 ---- python/djls/proto/v1/__init__.py | 4 + python/djls/proto/v1/check_pb2.py | 50 ++++ python/djls/proto/v1/check_pb2.pyi | 47 ++++ python/djls/proto/v1/django_pb2.py | 44 ++++ python/djls/proto/v1/django_pb2.pyi | 25 ++ python/djls/proto/v1/messages_pb2.py | 49 ++++ python/djls/proto/v1/messages_pb2.pyi | 63 +++++ python/djls/proto/v1/python_pb2.py | 66 +++++ python/djls/proto/v1/python_pb2.pyi | 156 ++++++++++++ 38 files changed, 1484 insertions(+), 685 deletions(-) create mode 100644 crates/djls-ipc/build.rs create mode 100644 crates/djls-ipc/src/proto.rs delete mode 100644 crates/djls-types/Cargo.toml delete mode 100644 crates/djls-types/build.rs delete mode 100644 crates/djls-types/src/lib.rs delete mode 100644 proto/messages.proto create mode 100644 proto/v1/check.proto create mode 100644 proto/v1/django.proto create mode 100644 proto/v1/messages.proto create mode 100644 proto/v1/python.proto create mode 100644 python/djls/_typing.py create mode 100644 python/djls/agent.py create mode 100644 python/djls/commands.py delete mode 100644 python/djls/messages_pb2.py create mode 100644 python/djls/proto/v1/__init__.py create mode 100644 python/djls/proto/v1/check_pb2.py create mode 100644 python/djls/proto/v1/check_pb2.pyi create mode 100644 python/djls/proto/v1/django_pb2.py create mode 100644 python/djls/proto/v1/django_pb2.pyi create mode 100644 python/djls/proto/v1/messages_pb2.py create mode 100644 python/djls/proto/v1/messages_pb2.pyi create mode 100644 python/djls/proto/v1/python_pb2.py create mode 100644 python/djls/proto/v1/python_pb2.pyi diff --git a/.editorconfig b/.editorconfig index f0821d3..e5da04a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ trim_trailing_whitespace = true [{,.}{j,J}ustfile] indent_size = 4 -[*.{py,rst,ini,md}] +[*.{just,proto,py,rst,ini,md}] indent_size = 4 [*.py] diff --git a/.just/proto.just b/.just/proto.just index 9f40084..175ba10 100644 --- a/.just/proto.just +++ b/.just/proto.just @@ -22,16 +22,84 @@ fmt: # Generate protobuf code for both Rust and Python [no-cd] gen: - @just proto rust - @just proto py + @just proto rust + @just proto py # Generate protobuf code for Rust [no-cd] rust: check - cargo build -p djls-types + @just proto clean-rust + cargo build -p djls-ipc + +[private] +clean-rust: + #!/usr/bin/env python3 + from pathlib import Path + import shutil + + target_dir = Path("{{ justfile_directory() }}/target") + + if target_dir.exists(): + for item in target_dir.rglob('*djls[_-]ipc*'): + if item.is_file(): + item.unlink() + elif item.is_dir(): + shutil.rmtree(item) # Generate protobuf code for Python [no-cd] py: check - protoc -I=proto --python_out=python/djls proto/*.proto + @just proto clean-py + protoc -I=proto \ + --python_out=python/djls/proto \ + --pyi_out=python/djls/proto \ + proto/v1/*.proto + fd -t f "(_pb2\.py|_pb2\.pyi)$" python/djls/proto -x sed -i 's/from v1 import/from . import/g' {} + fd -t d "." python/djls/proto -x touch {}//__init__.py + @just proto py-add-warnings +[private] +clean-py: + #!/usr/bin/env python3 + from pathlib import Path + import shutil + + proto_dir = Path("{{ justfile_directory() }}/python/djls/proto") + + for item in proto_dir.iterdir(): + if item.is_file(): + item.unlink() + elif item.is_dir(): + shutil.rmtree(item) + +[private] +py-add-warnings: + #!/usr/bin/env python3 + from pathlib import Path + + def create_warning(proto_file: str) -> str: + return f'''# WARNING: This file is generated by protobuf. DO NOT EDIT! + # Any changes made to this file will be overwritten when the protobuf files are regenerated. + # Source: {proto_file} + + ''' + + proto_dir = Path("{{ justfile_directory() }}/python/djls/proto") + proto_source_dir = Path("{{ justfile_directory() }}/proto") + + proto_sources = { + path.stem: path.relative_to(proto_source_dir) + for path in proto_source_dir.glob('**/*.proto') + } + + for file_path in proto_dir.glob("**/*.py*"): # Catches both .py and .pyi in all subdirs + proto_name = file_path.stem.removesuffix('_pb2') + source_proto = proto_sources.get(proto_name) + + content = file_path.read_text() + if not content.startswith('# WARNING'): + warning = create_warning( + str(source_proto) if source_proto + else "generated by py-init" + ) + file_path.write_text(warning + content) diff --git a/Cargo.toml b/Cargo.toml index 63b90f7..71dae51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ djls-django = { path = "crates/djls-django" } djls-ipc = { path = "crates/djls-ipc" } djls-python = { path = "crates/djls-python" } djls-server = { path = "crates/djls-server" } -djls-types = { path = "crates/djls-types" } djls-worker = { path = "crates/djls-worker" } anyhow = "1.0.94" diff --git a/Justfile b/Justfile index 29348a2..e1fbacf 100644 --- a/Justfile +++ b/Justfile @@ -8,6 +8,9 @@ mod proto ".just/proto.just" default: @just --list +clean: + rm -rf target/ + # run pre-commit on all files lint: @just --fmt diff --git a/crates/djlc-cli/src/main.rs b/crates/djlc-cli/src/main.rs index 2338779..385e208 100644 --- a/crates/djlc-cli/src/main.rs +++ b/crates/djlc-cli/src/main.rs @@ -1,5 +1,7 @@ use clap::{Args, Parser, Subcommand}; -use djls_ipc::{PythonProcess, Transport}; +use djls_ipc::v1::*; +use djls_ipc::{ProcessError, PythonProcess, TransportError}; +use std::ffi::OsStr; use std::time::Duration; #[derive(Debug, Parser)] @@ -41,8 +43,13 @@ async fn main() -> Result<(), Box> { match cli.command { Commands::Serve(opts) => { - let python = - PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?; + println!("Starting LSP server..."); + let python = PythonProcess::new::, &OsStr>( + "djls.agent", + None, + opts.health_check_interval(), + )?; + println!("LSP server started, beginning to serve..."); djls_server::serve(python).await? } } diff --git a/crates/djls-django/src/apps.rs b/crates/djls-django/src/apps.rs index 11c2c68..fa01aab 100644 --- a/crates/djls-django/src/apps.rs +++ b/crates/djls-django/src/apps.rs @@ -1,5 +1,5 @@ -use djls_ipc::{JsonResponse, PythonProcess, TransportError, TransportMessage, TransportResponse}; -use serde::Deserialize; +use djls_ipc::v1::*; +use djls_ipc::{ProcessError, PythonProcess}; use std::fmt; #[derive(Debug)] @@ -20,23 +20,6 @@ impl fmt::Display for App { #[derive(Debug, Default)] pub struct Apps(Vec); -#[derive(Debug, Deserialize)] -struct InstalledAppsCheck { - has_app: bool, -} - -impl TryFrom for InstalledAppsCheck { - type Error = TransportError; - - fn try_from(response: JsonResponse) -> Result { - response - .data() - .clone() - .ok_or_else(|| TransportError::Process("No data in response".to_string())) - .and_then(|data| serde_json::from_value(data).map_err(TransportError::Json)) - } -} - impl Apps { pub fn from_strings(apps: Vec) -> Self { Self(apps.into_iter().map(App).collect()) @@ -54,18 +37,21 @@ impl Apps { self.apps().iter() } - pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result { - let message = TransportMessage::Json("installed_apps_check".to_string()); - let response = python.send(message, Some(vec![app.to_string()]))?; - match response { - TransportResponse::Json(json_str) => { - let json_response: JsonResponse = serde_json::from_str(&json_str)?; - let result = InstalledAppsCheck::try_from(json_response)?; - Ok(result.has_app) - } - _ => Err(TransportError::Process( - "Unexpected response type".to_string(), + pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result { + let request = messages::Request { + command: Some(messages::request::Command::CheckAppInstalled( + check::AppInstalledRequest { + app_name: app.to_string(), + }, )), + }; + + let response = python.send(request).map_err(ProcessError::Transport)?; + + match response.result { + Some(messages::response::Result::CheckAppInstalled(response)) => Ok(response.passed), + Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)), + _ => Err(ProcessError::Response), } } } diff --git a/crates/djls-django/src/django.rs b/crates/djls-django/src/django.rs index d86be12..1d43352 100644 --- a/crates/djls-django/src/django.rs +++ b/crates/djls-django/src/django.rs @@ -1,63 +1,26 @@ -use crate::apps::Apps; use crate::gis::{check_gis_setup, GISError}; -use crate::templates::TemplateTags; -use djls_ipc::{JsonResponse, PythonProcess, TransportError, TransportMessage, TransportResponse}; +use djls_ipc::v1::*; +use djls_ipc::{ProcessError, PythonProcess, TransportError}; use djls_python::{ImportCheck, Python}; -use serde::Deserialize; use std::fmt; #[derive(Debug)] pub struct DjangoProject { py: Python, python: PythonProcess, - settings_module: String, - installed_apps: Apps, - templatetags: TemplateTags, -} - -#[derive(Debug, Deserialize)] -struct DjangoSetup { - installed_apps: Vec, - templatetags: TemplateTags, -} - -impl DjangoSetup { - pub fn setup(python: &mut PythonProcess) -> Result { - let message = TransportMessage::Json("django_setup".to_string()); - let response = python.send(message, None)?; - match response { - TransportResponse::Json(json_str) => { - let json_response: JsonResponse = serde_json::from_str(&json_str)?; - Ok(json_response) - } - _ => Err(ProjectError::Transport(TransportError::Process( - "Unexpected response type".to_string(), - ))), - } - } + version: String, } impl DjangoProject { - fn new( - py: Python, - python: PythonProcess, - settings_module: String, - installed_apps: Apps, - templatetags: TemplateTags, - ) -> Self { + fn new(py: Python, python: PythonProcess, version: String) -> Self { Self { py, python, - settings_module, - installed_apps, - templatetags, + version, } } pub fn setup(mut python: PythonProcess) -> Result { - let settings_module = - std::env::var("DJANGO_SETTINGS_MODULE").expect("DJANGO_SETTINGS_MODULE must be set"); - let py = Python::setup(&mut python)?; let has_django = ImportCheck::check(&mut python, Some(vec!["django".to_string()]))?; @@ -74,45 +37,52 @@ impl DjangoProject { return Ok(Self { py, python, - settings_module, - installed_apps: Apps::default(), - templatetags: TemplateTags::default(), + version: String::new(), }); } - let response = DjangoSetup::setup(&mut python)?; - let setup: DjangoSetup = response - .data() - .clone() - .ok_or_else(|| TransportError::Process("No data in response".to_string())) - .and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))?; + let request = messages::Request { + command: Some(messages::request::Command::DjangoGetProjectInfo( + django::GetProjectInfoRequest {}, + )), + }; - Ok(Self::new( + let response = python + .send(request) + .map_err(|e| ProjectError::Transport(e))?; + + let version = match response.result { + Some(messages::response::Result::DjangoGetProjectInfo(response)) => { + response.project.unwrap().version + } + Some(messages::response::Result::Error(e)) => { + return Err(ProjectError::Process(ProcessError::Health(e.message))); + } + _ => { + return Err(ProjectError::Process(ProcessError::Response)); + } + }; + + Ok(Self { py, python, - settings_module, - Apps::from_strings(setup.installed_apps.to_vec()), - setup.templatetags, - )) + version, + }) } pub fn py(&self) -> &Python { &self.py } - fn settings_module(&self) -> &String { - &self.settings_module + fn version(&self) -> &String { + &self.version } } impl fmt::Display for DjangoProject { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Django Project")?; - writeln!(f, "Settings Module: {}", self.settings_module)?; - writeln!(f, "Installed Apps:")?; - write!(f, "{}", self.installed_apps)?; - writeln!(f, "Template Tags:")?; - write!(f, "{}", self.templatetags)?; + writeln!(f, "Version: {}", self.version)?; Ok(()) } } @@ -121,22 +91,18 @@ impl fmt::Display for DjangoProject { pub enum ProjectError { #[error("Django is not installed or cannot be imported")] DjangoNotFound, - #[error("IO error: {0}")] Io(#[from] std::io::Error), - #[error("GIS error: {0}")] Gis(#[from] GISError), - #[error("JSON parsing error: {0}")] Json(#[from] serde_json::Error), - #[error(transparent)] Packaging(#[from] djls_python::PackagingError), - + #[error("Process error: {0}")] + Process(#[from] ProcessError), #[error(transparent)] Python(#[from] djls_python::PythonError), - #[error("Transport error: {0}")] Transport(#[from] TransportError), } diff --git a/crates/djls-django/src/gis.rs b/crates/djls-django/src/gis.rs index d5702bb..bd817bd 100644 --- a/crates/djls-django/src/gis.rs +++ b/crates/djls-django/src/gis.rs @@ -1,5 +1,5 @@ use crate::apps::Apps; -use djls_ipc::{PythonProcess, TransportError}; +use djls_ipc::{ProcessError, PythonProcess, TransportError}; use std::process::Command; pub fn check_gis_setup(python: &mut PythonProcess) -> Result { @@ -17,10 +17,10 @@ pub fn check_gis_setup(python: &mut PythonProcess) -> Result { pub enum GISError { #[error("IO error: {0}")] Io(#[from] std::io::Error), - #[error("JSON parsing error: {0}")] Json(#[from] serde_json::Error), - + #[error("Process error: {0}")] + Process(#[from] ProcessError), #[error("Transport error: {0}")] Transport(#[from] TransportError), } diff --git a/crates/djls-ipc/Cargo.toml b/crates/djls-ipc/Cargo.toml index a73957f..9816c2a 100644 --- a/crates/djls-ipc/Cargo.toml +++ b/crates/djls-ipc/Cargo.toml @@ -4,15 +4,16 @@ version = "0.0.0" edition = "2021" [dependencies] -djls-types = { workspace = true } - anyhow = { workspace = true } async-trait = { workspace = true } -prost = { workspace = true } -bytes = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } -tempfile = "3.14.0" +bytes = "1.9" +prost = "0.13" +tempfile = "3.14" + +[build-dependencies] +prost-build = "0.13" diff --git a/crates/djls-ipc/build.rs b/crates/djls-ipc/build.rs new file mode 100644 index 0000000..ce9dd36 --- /dev/null +++ b/crates/djls-ipc/build.rs @@ -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 { + 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::>(), + &[proto_dir], + ) + .unwrap(); +} diff --git a/crates/djls-ipc/src/lib.rs b/crates/djls-ipc/src/lib.rs index 21e0519..cb79255 100644 --- a/crates/djls-ipc/src/lib.rs +++ b/crates/djls-ipc/src/lib.rs @@ -1,11 +1,9 @@ mod process; +mod proto; mod transport; +pub use process::ProcessError; pub use process::PythonProcess; -pub use transport::parse_json_response; -pub use transport::parse_raw_response; -pub use transport::JsonResponse; +pub use proto::v1; pub use transport::Transport; pub use transport::TransportError; -pub use transport::TransportMessage; -pub use transport::TransportResponse; diff --git a/crates/djls-ipc/src/process.rs b/crates/djls-ipc/src/process.rs index 922c745..f317f15 100644 --- a/crates/djls-ipc/src/process.rs +++ b/crates/djls-ipc/src/process.rs @@ -1,6 +1,6 @@ -use crate::transport::{ - Transport, TransportError, TransportMessage, TransportProtocol, TransportResponse, -}; +use crate::proto::v1::*; +use crate::transport::{Transport, TransportError}; +use std::ffi::OsStr; use std::process::{Child, Command, Stdio}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; @@ -9,75 +9,130 @@ use tokio::time; #[derive(Debug)] pub struct PythonProcess { - transport: Arc>>, + transport: Arc>, _child: Child, healthy: Arc, } impl PythonProcess { - pub fn new( + pub fn new( module: &str, - transport: Transport, + args: Option, health_check_interval: Option, - ) -> Result { - let mut child = Command::new("python") - .arg("-m") - .arg(module) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn()?; + ) -> Result + where + I: IntoIterator, + S: AsRef, + { + let mut command = Command::new("python"); + command.arg("-m").arg(module); + + if let Some(args) = args { + command.args(args); + } + + command.stdin(Stdio::piped()).stdout(Stdio::piped()); + + let mut child = command.spawn().map_err(TransportError::Io)?; let stdin = child.stdin.take().unwrap(); let stdout = child.stdout.take().unwrap(); + let transport = Transport::new(stdin, stdout)?; + let process = Self { - transport: Arc::new(Mutex::new(transport.create(stdin, stdout)?)), + transport: Arc::new(Mutex::new(transport)), _child: child, healthy: Arc::new(AtomicBool::new(true)), }; if let Some(interval) = health_check_interval { - process.start_health_check_task(interval)?; + let transport = process.transport.clone(); + let healthy = process.healthy.clone(); + tokio::spawn(async move { + let mut interval = time::interval(interval); + loop { + interval.tick().await; + let _ = PythonProcess::check_health(transport.clone(), healthy.clone()).await; + } + }); } Ok(process) } - fn start_health_check_task(&self, interval: Duration) -> Result<(), TransportError> { - let healthy = self.healthy.clone(); - let transport = self.transport.clone(); - - tokio::spawn(async move { - let mut interval = time::interval(interval); - loop { - interval.tick().await; - - if let Ok(mut transport) = transport.lock() { - match transport.health_check() { - Ok(()) => { - healthy.store(true, Ordering::SeqCst); - } - Err(_) => { - healthy.store(false, Ordering::SeqCst); - } - } - } - } - }); - - Ok(()) - } - pub fn is_healthy(&self) -> bool { self.healthy.load(Ordering::SeqCst) } pub fn send( &mut self, - message: TransportMessage, - args: Option>, - ) -> Result { + request: messages::Request, + ) -> Result { let mut transport = self.transport.lock().unwrap(); - transport.send(message, args) + transport.send(request) + } + + async fn check_health( + transport: Arc>, + healthy: Arc, + ) -> 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), +} diff --git a/crates/djls-ipc/src/proto.rs b/crates/djls-ipc/src/proto.rs new file mode 100644 index 0000000..1be4bc9 --- /dev/null +++ b/crates/djls-ipc/src/proto.rs @@ -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")); + } +} diff --git a/crates/djls-ipc/src/transport.rs b/crates/djls-ipc/src/transport.rs index 510f297..9284421 100644 --- a/crates/djls-ipc/src/transport.rs +++ b/crates/djls-ipc/src/transport.rs @@ -1,320 +1,59 @@ -use djls_types::proto::*; +use crate::process::ProcessError; +use crate::proto::v1::*; use prost::Message; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::fmt::Debug; -use std::io::Read; -use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::io::{BufRead, BufReader, BufWriter, Read, Write}; use std::process::{ChildStdin, ChildStdout}; use std::sync::{Arc, Mutex}; -use thiserror::Error; -#[derive(Error, Debug)] -pub enum TransportError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - #[error("JSON error: {0}")] - Json(#[from] serde_json::Error), - #[error("Process error: {0}")] - Process(String), -} - -pub enum Transport { - Raw, - Json, - Protobuf, +#[derive(Debug, Clone)] +pub struct Transport { + reader: Arc>>, + writer: Arc>>, } impl Transport { - pub fn create( - &self, - mut stdin: ChildStdin, - mut stdout: ChildStdout, - ) -> Result, TransportError> { - let transport_type = match self { - Transport::Raw => "raw", - Transport::Json => "json", - Transport::Protobuf => "protobuf", - }; - - writeln!(stdin, "{}", transport_type).map_err(TransportError::Io)?; + pub fn new(mut stdin: ChildStdin, mut stdout: ChildStdout) -> Result { stdin.flush().map_err(TransportError::Io)?; let mut ready_line = String::new(); BufReader::new(&mut stdout) .read_line(&mut ready_line) .map_err(TransportError::Io)?; + if ready_line.trim() != "ready" { - return Err(TransportError::Process( - "Python process not ready".to_string(), - )); + return Err(ProcessError::Ready("Python process not ready".to_string())); } - match self { - Transport::Raw => Ok(Box::new(RawTransport::new(stdin, stdout)?)), - Transport::Json => Ok(Box::new(JsonTransport::new(stdin, stdout)?)), - Transport::Protobuf => Ok(Box::new(ProtobufTransport::new(stdin, stdout)?)), - } - } -} - -#[derive(Debug)] -pub enum TransportMessage { - Raw(String), - Json(String), - Protobuf(ToAgent), -} - -#[derive(Debug)] -pub enum TransportResponse { - Raw(String), - Json(String), - Protobuf(FromAgent), -} - -pub trait TransportProtocol: Debug + Send { - fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result - where - Self: Sized; - fn health_check(&mut self) -> Result<(), TransportError>; - fn clone_box(&self) -> Box; - fn send_impl( - &mut self, - message: TransportMessage, - args: Option>, - ) -> Result; - - fn send( - &mut self, - message: TransportMessage, - args: Option>, - ) -> Result { - self.health_check()?; - self.send_impl(message, args) - } -} - -impl Clone for Box { - fn clone(&self) -> Self { - self.clone_box() - } -} - -#[derive(Debug)] -pub struct RawTransport { - reader: Arc>>, - writer: Arc>>, -} - -impl TransportProtocol for RawTransport { - fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result { Ok(Self { reader: Arc::new(Mutex::new(BufReader::new(stdout))), writer: Arc::new(Mutex::new(BufWriter::new(stdin))), }) } - fn health_check(&mut self) -> Result<(), TransportError> { - self.send_impl(TransportMessage::Raw("health".to_string()), None) - .and_then(|response| match response { - TransportResponse::Raw(s) if s == "ok" => Ok(()), - TransportResponse::Raw(other) => Err(TransportError::Process(format!( - "Health check failed: {}", - other - ))), - _ => Err(TransportError::Process( - "Unexpected response type".to_string(), - )), - }) - } - - fn clone_box(&self) -> Box { - Box::new(RawTransport { - reader: self.reader.clone(), - writer: self.writer.clone(), - }) - } - - fn send_impl( + pub fn send( &mut self, - message: TransportMessage, - args: Option>, - ) -> Result { - let mut writer = self.writer.lock().unwrap(); - - match message { - TransportMessage::Raw(msg) => { - if let Some(args) = args { - writeln!(writer, "{} {}", msg, args.join(" ")).map_err(TransportError::Io)?; - } else { - writeln!(writer, "{}", msg).map_err(TransportError::Io)?; - } - } - _ => { - return Err(TransportError::Process( - "Raw transport only accepts raw messages".to_string(), - )) - } - } + message: messages::Request, + ) -> Result { + let buf = message.encode_to_vec(); + let mut writer = self.writer.lock().map_err(|_| { + TransportError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to acquire writer lock", + )) + })?; + writer + .write_all(&(buf.len() as u32).to_be_bytes()) + .map_err(TransportError::Io)?; + writer.write_all(&buf).map_err(TransportError::Io)?; writer.flush().map_err(TransportError::Io)?; - let mut reader = self.reader.lock().unwrap(); - let mut line = String::new(); - reader.read_line(&mut line).map_err(TransportError::Io)?; - Ok(TransportResponse::Raw(line.trim().to_string())) - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct JsonCommand { - command: String, - args: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct JsonResponse { - status: String, - data: Option, - error: Option, -} - -impl JsonResponse { - pub fn data(&self) -> &Option { - &self.data - } -} - -#[derive(Debug)] -pub struct JsonTransport { - reader: Arc>>, - writer: Arc>>, -} - -impl TransportProtocol for JsonTransport { - fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result { - 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 { - Box::new(JsonTransport { - reader: self.reader.clone(), - writer: self.writer.clone(), - }) - } - - fn send_impl( - &mut self, - message: TransportMessage, - args: Option>, - ) -> Result { - 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>>, - writer: Arc>>, -} - -impl TransportProtocol for ProtobufTransport { - fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result { - 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 { - Box::new(ProtobufTransport { - reader: self.reader.clone(), - writer: self.writer.clone(), - }) - } - - fn send_impl( - &mut self, - message: TransportMessage, - _args: Option>, - ) -> Result { - let mut writer = self.writer.lock().unwrap(); - - match message { - TransportMessage::Protobuf(msg) => { - let buf = msg.encode_to_vec(); - writer - .write_all(&(buf.len() as u32).to_be_bytes()) - .map_err(TransportError::Io)?; - writer.write_all(&buf).map_err(TransportError::Io)?; - } - _ => { - return Err(TransportError::Process( - "Protobuf transport only accepts protobuf messages".to_string(), - )) - } - } - - writer.flush().map_err(TransportError::Io)?; - - let mut reader = self.reader.lock().unwrap(); + let mut reader = self.reader.lock().map_err(|_| { + TransportError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to acquire reader lock", + )) + })?; let mut length_bytes = [0u8; 4]; reader .read_exact(&mut length_bytes) @@ -326,17 +65,17 @@ impl TransportProtocol for ProtobufTransport { .read_exact(&mut message_bytes) .map_err(TransportError::Io)?; - let response = FromAgent::decode(message_bytes.as_slice()) - .map_err(|e| TransportError::Process(e.to_string()))?; - - Ok(TransportResponse::Protobuf(response)) + messages::Response::decode(message_bytes.as_slice()) + .map_err(|e| TransportError::Decode(e.to_string())) } } -pub fn parse_raw_response(response: String) -> Result { - Ok(response) -} - -pub fn parse_json_response(response: String) -> Result { - serde_json::from_str(&response).map_err(TransportError::Json) +#[derive(thiserror::Error, Debug)] +pub enum TransportError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Task error: {0}")] + Task(#[from] tokio::task::JoinError), + #[error("Failed to decode message: {0}")] + Decode(String), } diff --git a/crates/djls-python/src/packaging.rs b/crates/djls-python/src/packaging.rs index b90b6ce..2c9bd17 100644 --- a/crates/djls-python/src/packaging.rs +++ b/crates/djls-python/src/packaging.rs @@ -1,4 +1,5 @@ -use djls_ipc::{JsonResponse, PythonProcess, TransportError, TransportMessage, TransportResponse}; +use djls_ipc::v1::*; +use djls_ipc::{ProcessError, PythonProcess, TransportError}; use serde::Deserialize; use std::collections::HashMap; use std::fmt; @@ -6,15 +7,25 @@ use std::path::PathBuf; #[derive(Clone, Debug, Deserialize)] pub struct Package { - name: String, - version: String, - location: Option, + dist_name: String, + dist_version: String, + dist_location: Option, +} + +impl From for Package { + fn from(p: python::Package) -> Self { + Package { + dist_name: p.dist_name, + dist_version: p.dist_version, + dist_location: p.dist_location.map(PathBuf::from), + } + } } impl fmt::Display for Package { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", self.name, self.version)?; - if let Some(location) = &self.location { + write!(f, "{} {}", self.dist_name, self.dist_version)?; + if let Some(location) = &self.dist_location { write!(f, " ({})", location.display())?; } Ok(()) @@ -30,6 +41,12 @@ impl Packages { } } +impl From> for Packages { + fn from(packages: HashMap) -> Self { + Packages(packages.into_iter().map(|(k, v)| (k, v.into())).collect()) + } +} + impl FromIterator<(String, Package)> for Packages { fn from_iter>(iter: T) -> Self { Self(HashMap::from_iter(iter)) @@ -39,7 +56,7 @@ impl FromIterator<(String, Package)> for Packages { impl fmt::Display for Packages { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut packages: Vec<_> = self.packages(); - packages.sort_by(|a, b| a.name.cmp(&b.name)); + packages.sort_by(|a, b| a.dist_name.cmp(&b.dist_name)); if packages.is_empty() { writeln!(f, " (no packages installed)")?; @@ -57,18 +74,6 @@ pub struct ImportCheck { can_import: bool, } -impl TryFrom for ImportCheck { - type Error = TransportError; - - fn try_from(response: JsonResponse) -> Result { - response - .data() - .clone() - .ok_or_else(|| TransportError::Process("No data in response".to_string())) - .and_then(|data| serde_json::from_value(data).map_err(TransportError::Json)) - } -} - impl ImportCheck { pub fn can_import(&self) -> bool { self.can_import @@ -76,19 +81,24 @@ impl ImportCheck { pub fn check( python: &mut PythonProcess, - modules: Option>, + _modules: Option>, ) -> Result { - let message = TransportMessage::Json("has_import".to_string()); - let response = python.send(message, modules)?; - match response { - TransportResponse::Json(json_str) => { - let json_response: JsonResponse = serde_json::from_str(&json_str)?; - let check = Self::try_from(json_response)?; - Ok(check.can_import) + let request = messages::Request { + command: Some(messages::request::Command::CheckDjangoAvailable( + check::DjangoAvailableRequest {}, + )), + }; + + let response = python + .send(request) + .map_err(|e| PackagingError::Transport(e))?; + + match response.result { + Some(messages::response::Result::CheckDjangoAvailable(response)) => Ok(response.passed), + Some(messages::response::Result::Error(e)) => { + Err(PackagingError::Process(ProcessError::Health(e.message))) } - _ => Err(PackagingError::Transport(TransportError::Process( - "Unexpected response type".to_string(), - ))), + _ => Err(PackagingError::Process(ProcessError::Response)), } } } @@ -103,6 +113,8 @@ pub enum PackagingError { #[error("Transport error: {0}")] Transport(#[from] TransportError), + #[error("Process error: {0}")] + Process(#[from] ProcessError), #[error("UTF-8 conversion error: {0}")] Utf8(#[from] std::string::FromUtf8Error), diff --git a/crates/djls-python/src/python.rs b/crates/djls-python/src/python.rs index 5c43bee..e14d549 100644 --- a/crates/djls-python/src/python.rs +++ b/crates/djls-python/src/python.rs @@ -1,5 +1,6 @@ use crate::packaging::{Packages, PackagingError}; -use djls_ipc::{JsonResponse, PythonProcess, TransportError, TransportMessage, TransportResponse}; +use djls_ipc::v1::*; +use djls_ipc::{ProcessError, PythonProcess, TransportError}; use serde::Deserialize; use std::fmt; use std::path::PathBuf; @@ -8,16 +9,45 @@ use std::path::PathBuf; pub struct VersionInfo { major: u8, minor: u8, - patch: u8, - suffix: Option, + micro: u8, + releaselevel: ReleaseLevel, + serial: Option, +} + +impl From 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 for ReleaseLevel { + fn from(level: python::ReleaseLevel) -> Self { + match level { + python::ReleaseLevel::Alpha => ReleaseLevel::Alpha, + python::ReleaseLevel::Beta => ReleaseLevel::Beta, + python::ReleaseLevel::Candidate => ReleaseLevel::Candidate, + python::ReleaseLevel::Final => ReleaseLevel::Final, + } + } } impl fmt::Display for VersionInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?; - if let Some(suffix) = &self.suffix { - write!(f, "{}", suffix)?; - } + write!(f, "{}.{}.{}", self.major, self.minor, self.micro)?; Ok(()) } } @@ -58,30 +88,52 @@ pub struct Python { packages: Packages, } -impl TryFrom for Python { - type Error = TransportError; +impl Python { + pub fn setup(python: &mut PythonProcess) -> Result { + let request = messages::Request { + command: Some(messages::request::Command::PythonGetEnvironment( + python::GetEnvironmentRequest {}, + )), + }; - fn try_from(response: JsonResponse) -> Result { - response - .data() - .clone() - .ok_or_else(|| TransportError::Process("No data in response".to_string())) - .and_then(|data| serde_json::from_value(data).map_err(TransportError::Json)) + let response = python.send(request).map_err(PythonError::Transport)?; + + match response.result { + Some(messages::response::Result::PythonGetEnvironment(response)) => response + .python + .ok_or_else(|| PythonError::Process(ProcessError::Response)) + .map(Into::into), + Some(messages::response::Result::Error(e)) => { + Err(PythonError::Process(ProcessError::Health(e.message))) + } + _ => Err(PythonError::Process(ProcessError::Response)), + } } } -impl Python { - pub fn setup(python: &mut PythonProcess) -> Result { - let message = TransportMessage::Json("python_setup".to_string()); - let response = python.send(message, None)?; - match response { - TransportResponse::Json(json_str) => { - let json_response: JsonResponse = serde_json::from_str(&json_str)?; - Ok(Self::try_from(json_response)?) - } - _ => Err(PythonError::Transport(TransportError::Process( - "Unexpected response type".to_string(), - ))), +impl From for Python { + fn from(p: python::Python) -> Self { + let sys = p.sys.unwrap(); + let sysconfig = p.sysconfig.unwrap(); + let site = p.site.unwrap(); + + Self { + version_info: sys.version_info.unwrap_or_default().into(), + sysconfig_paths: SysconfigPaths { + data: PathBuf::from(sysconfig.data), + include: PathBuf::from(sysconfig.include), + platinclude: PathBuf::from(sysconfig.platinclude), + platlib: PathBuf::from(sysconfig.platlib), + platstdlib: PathBuf::from(sysconfig.platstdlib), + purelib: PathBuf::from(sysconfig.purelib), + scripts: PathBuf::from(sysconfig.scripts), + stdlib: PathBuf::from(sysconfig.stdlib), + }, + sys_prefix: PathBuf::from(sys.prefix), + sys_base_prefix: PathBuf::from(sys.base_prefix), + sys_executable: PathBuf::from(sys.executable), + sys_path: sys.path.into_iter().map(PathBuf::from).collect(), + packages: site.packages.into(), } } } @@ -107,25 +159,20 @@ impl fmt::Display for Python { pub enum PythonError { #[error("Python execution failed: {0}")] Execution(String), - #[error("IO error: {0}")] Io(#[from] std::io::Error), - #[error("JSON parsing error: {0}")] Json(#[from] serde_json::Error), - #[error("Packaging error: {0}")] Packaging(#[from] PackagingError), - #[error("Integer parsing error: {0}")] Parse(#[from] std::num::ParseIntError), - + #[error("Process error: {0}")] + Process(#[from] ProcessError), #[error("Failed to locate Python executable: {0}")] PythonNotFound(#[from] which::Error), - #[error("Transport error: {0}")] Transport(#[from] TransportError), - #[error("UTF-8 conversion error: {0}")] Utf8(#[from] std::string::FromUtf8Error), } diff --git a/crates/djls-types/Cargo.toml b/crates/djls-types/Cargo.toml deleted file mode 100644 index 0025c42..0000000 --- a/crates/djls-types/Cargo.toml +++ /dev/null @@ -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" diff --git a/crates/djls-types/build.rs b/crates/djls-types/build.rs deleted file mode 100644 index 596e628..0000000 --- a/crates/djls-types/build.rs +++ /dev/null @@ -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::>(), - &[proto_dir], - ) - .unwrap(); -} diff --git a/crates/djls-types/src/lib.rs b/crates/djls-types/src/lib.rs deleted file mode 100644 index 65f6aa0..0000000 --- a/crates/djls-types/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod proto { - include!(concat!(env!("OUT_DIR"), "/djls.rs")); -} - -use proto::*; diff --git a/proto/messages.proto b/proto/messages.proto deleted file mode 100644 index ba0dc34..0000000 --- a/proto/messages.proto +++ /dev/null @@ -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 -} diff --git a/proto/v1/check.proto b/proto/v1/check.proto new file mode 100644 index 0000000..1993013 --- /dev/null +++ b/proto/v1/check.proto @@ -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; +} diff --git a/proto/v1/django.proto b/proto/v1/django.proto new file mode 100644 index 0000000..27c24da --- /dev/null +++ b/proto/v1/django.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package djls.v1.django; + +// models +message Project { + string version = 3; +} + +// commands +message GetProjectInfoRequest {} + +message GetProjectInfoResponse { + Project project = 1; +} diff --git a/proto/v1/messages.proto b/proto/v1/messages.proto new file mode 100644 index 0000000..c2ed8fe --- /dev/null +++ b/proto/v1/messages.proto @@ -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; + } +} diff --git a/proto/v1/python.proto b/proto/v1/python.proto new file mode 100644 index 0000000..6cb23c0 --- /dev/null +++ b/proto/v1/python.proto @@ -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 environ = 1; +} + +message Site { + map 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; +} diff --git a/python/djls/_typing.py b/python/djls/_typing.py new file mode 100644 index 0000000..8e8b0eb --- /dev/null +++ b/python/djls/_typing.py @@ -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 diff --git a/python/djls/agent.py b/python/djls/agent.py new file mode 100644 index 0000000..bf3835c --- /dev/null +++ b/python/djls/agent.py @@ -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() diff --git a/python/djls/commands.py b/python/djls/commands.py new file mode 100644 index 0000000..21fec0c --- /dev/null +++ b/python/djls/commands.py @@ -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, +] diff --git a/python/djls/lsp.py b/python/djls/lsp.py index fad874f..1e391e7 100644 --- a/python/djls/lsp.py +++ b/python/djls/lsp.py @@ -9,34 +9,6 @@ from .scripts import has_import from .scripts import python_setup -def handle_command(command: str) -> str: - parts = command.strip().split() - command = parts[0] - args = parts[1:] if len(parts) > 1 else [] - - if command == "django_setup": - return json.dumps(django_setup.get_django_setup_info()) - if command == "has_import": - if not args: - return "error: Missing module name argument" - return json.dumps({"can_import": has_import.check_import(args[0])}) - if command == "health": - return "ok" - if command == "installed_apps_check": - import django - from django.conf import settings - - django.setup() - if not args: - return "error: Missing module name argument" - return json.dumps({"has_app": args[0] in settings.INSTALLED_APPS}) - if command == "python_setup": - return json.dumps(python_setup.get_python_info()) - if command == "version": - return "0.1.0" - return f"Unknown command: {command}" - - def handle_json_command(data: dict[str, Any]) -> dict[str, Any]: command = data["command"] args = data.get("args", []) # Get args if they exist @@ -84,20 +56,12 @@ def main(): if not line: break - if transport_type == "json": - data = json.loads(line) - response = handle_json_command(data) - print(json.dumps(response), flush=True) - else: - command = line.strip() - response = handle_command(command) - print(response, flush=True) + data = json.loads(line) + response = handle_json_command(data) + print(json.dumps(response), flush=True) except Exception as e: - if transport_type == "json": - print(json.dumps({"status": "error", "error": str(e)}), flush=True) - else: - print(f"error: {str(e)}", flush=True) + print(json.dumps({"status": "error", "error": str(e)}), flush=True) if __name__ == "__main__": diff --git a/python/djls/messages_pb2.py b/python/djls/messages_pb2.py deleted file mode 100644 index 4a516b8..0000000 --- a/python/djls/messages_pb2.py +++ /dev/null @@ -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) diff --git a/python/djls/proto/v1/__init__.py b/python/djls/proto/v1/__init__.py new file mode 100644 index 0000000..27ddd73 --- /dev/null +++ b/python/djls/proto/v1/__init__.py @@ -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 + diff --git a/python/djls/proto/v1/check_pb2.py b/python/djls/proto/v1/check_pb2.py new file mode 100644 index 0000000..8de9965 --- /dev/null +++ b/python/djls/proto/v1/check_pb2.py @@ -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) diff --git a/python/djls/proto/v1/check_pb2.pyi b/python/djls/proto/v1/check_pb2.pyi new file mode 100644 index 0000000..1811cc7 --- /dev/null +++ b/python/djls/proto/v1/check_pb2.pyi @@ -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: ... diff --git a/python/djls/proto/v1/django_pb2.py b/python/djls/proto/v1/django_pb2.py new file mode 100644 index 0000000..1e798d1 --- /dev/null +++ b/python/djls/proto/v1/django_pb2.py @@ -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) diff --git a/python/djls/proto/v1/django_pb2.pyi b/python/djls/proto/v1/django_pb2.pyi new file mode 100644 index 0000000..e4a5fc8 --- /dev/null +++ b/python/djls/proto/v1/django_pb2.pyi @@ -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: ... diff --git a/python/djls/proto/v1/messages_pb2.py b/python/djls/proto/v1/messages_pb2.py new file mode 100644 index 0000000..aaa0fee --- /dev/null +++ b/python/djls/proto/v1/messages_pb2.py @@ -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) diff --git a/python/djls/proto/v1/messages_pb2.pyi b/python/djls/proto/v1/messages_pb2.pyi new file mode 100644 index 0000000..47db32d --- /dev/null +++ b/python/djls/proto/v1/messages_pb2.pyi @@ -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: ... diff --git a/python/djls/proto/v1/python_pb2.py b/python/djls/proto/v1/python_pb2.py new file mode 100644 index 0000000..6732944 --- /dev/null +++ b/python/djls/proto/v1/python_pb2.py @@ -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) diff --git a/python/djls/proto/v1/python_pb2.pyi b/python/djls/proto/v1/python_pb2.pyi new file mode 100644 index 0000000..a5633cc --- /dev/null +++ b/python/djls/proto/v1/python_pb2.pyi @@ -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: ...