add djls-django crate (#6)

* add djls-django crate

* rework

* oops

* add check for GDAL and GeoDjango

* lots of things

* remove unused scripts

* move scripts to dedicated mod and make static consts

* inline gdal check

* rename mod

* rename mod

* move server info to consts

* adjust pyproject

* hide rustfmt config

* simplify django setup

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

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[env]
CARGO_WORKSPACE_DIR = { value = "", relative = true }

View file

@ -2,7 +2,19 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/backplane/pre-commit-rust-hooks
rev: v1.1.0
hooks:
- id: fmt
- id: check
- id: clippy
- id: test

2
.rustfmt.toml Normal file
View file

@ -0,0 +1,2 @@
imports_granularity = "Item"
unstable_features = true

View file

@ -4,6 +4,7 @@ resolver = "2"
[workspace.dependencies]
djls = { path = "crates/djls" }
djls-django = { path = "crates/djls-django" }
djls-python = { path = "crates/djls-python" }
anyhow = "1.0.94"

12
Justfile Normal file
View file

@ -0,0 +1,12 @@
set dotenv-load := true
set unstable := true
# List all available commands
[private]
default:
@just --list
# run pre-commit on all files
lint:
@just --fmt
uv run --with pre-commit-uv pre-commit run --all-files

View file

@ -7,4 +7,3 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,4 +1,3 @@
# django-language-server
A Language Server Protocol implementation for Django projects.

View file

@ -0,0 +1,12 @@
[package]
name = "djls-django"
version = "0.0.0"
edition = "2021"
[dependencies]
djls-python = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = "2.0.4"

View file

@ -0,0 +1,64 @@
use djls_python::{Python, RunnerError, ScriptRunner};
use serde::Deserialize;
use std::fmt;
use crate::scripts;
#[derive(Debug)]
pub struct App(String);
impl App {
pub fn name(&self) -> &str {
&self.0
}
}
impl fmt::Display for App {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Default)]
pub struct Apps(Vec<App>);
#[derive(Debug, Deserialize)]
struct InstalledAppsCheck {
has_app: bool,
}
impl ScriptRunner for InstalledAppsCheck {
const SCRIPT: &'static str = scripts::INSTALLED_APPS_CHECK;
}
impl Apps {
pub fn from_strings(apps: Vec<String>) -> Self {
Self(apps.into_iter().map(App).collect())
}
pub fn apps(&self) -> &[App] {
&self.0
}
pub fn has_app(&self, name: &str) -> bool {
self.0.iter().any(|app| app.0 == name)
}
pub fn iter(&self) -> impl Iterator<Item = &App> {
self.0.iter()
}
pub fn check_installed(py: &Python, app: &str) -> Result<bool, RunnerError> {
let result = InstalledAppsCheck::run_with_py_args(py, app)?;
Ok(result.has_app)
}
}
impl fmt::Display for Apps {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for app in &self.0 {
writeln!(f, " {}", app)?;
}
Ok(())
}
}

View file

@ -0,0 +1,120 @@
use crate::apps::Apps;
use crate::gis::{check_gis_setup, GISError};
use crate::scripts;
use crate::templates::TemplateTag;
use djls_python::{ImportCheck, Python, RunnerError, ScriptRunner};
use serde::Deserialize;
use std::fmt;
#[derive(Debug)]
pub struct DjangoProject {
py: Python,
settings_module: String,
installed_apps: Apps,
templatetags: Vec<TemplateTag>,
}
#[derive(Debug, Deserialize)]
struct DjangoSetup {
installed_apps: Vec<String>,
templatetags: Vec<TemplateTag>,
}
impl ScriptRunner for DjangoSetup {
const SCRIPT: &'static str = scripts::DJANGO_SETUP;
}
impl DjangoProject {
fn new(
py: Python,
settings_module: String,
installed_apps: Apps,
templatetags: Vec<TemplateTag>,
) -> Self {
Self {
py,
settings_module,
installed_apps,
templatetags,
}
}
pub fn setup() -> Result<Self, ProjectError> {
let settings_module =
std::env::var("DJANGO_SETTINGS_MODULE").expect("DJANGO_SETTINGS_MODULE must be set");
let py = Python::initialize()?;
let has_django = ImportCheck::check(&py, "django")?;
if !has_django {
return Err(ProjectError::DjangoNotFound);
}
if !check_gis_setup(&py)? {
eprintln!("Warning: GeoDjango detected but GDAL is not available.");
eprintln!("Django initialization will be skipped. Some features may be limited.");
eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites.");
return Ok(Self {
py,
settings_module,
installed_apps: Apps::default(),
templatetags: Vec::new(),
});
}
let setup = DjangoSetup::run_with_py(&py)?;
Ok(Self::new(
py,
settings_module,
Apps::from_strings(setup.installed_apps.to_vec()),
setup.templatetags.to_vec(),
))
}
pub fn py(&self) -> &Python {
&self.py
}
fn settings_module(&self) -> &String {
&self.settings_module
}
}
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:")?;
for tag in &self.templatetags {
write!(f, "{}", tag)?;
writeln!(f)?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
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)]
Python(#[from] djls_python::PythonError),
#[error(transparent)]
Runner(#[from] RunnerError),
}

View file

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

View file

@ -0,0 +1,7 @@
mod apps;
mod django;
mod gis;
mod scripts;
mod templates;
pub use django::DjangoProject;

View file

@ -0,0 +1,4 @@
use djls_python::include_script;
pub const DJANGO_SETUP: &str = include_script!("django_setup");
pub const INSTALLED_APPS_CHECK: &str = include_script!("installed_apps_check");

View file

@ -0,0 +1,30 @@
use serde::Deserialize;
use std::fmt;
#[derive(Clone, Debug, Deserialize)]
pub struct TemplateTag {
name: String,
library: String,
doc: Option<String>,
}
impl fmt::Display for TemplateTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let library = if self.library.is_empty() {
"builtins"
} else {
&self.library
};
write!(f, "{} ({})", self.name, library)?;
writeln!(f)?;
if let Some(doc) = &self.doc {
for line in doc.trim_end().split("\n") {
writeln!(f, "{}", line)?;
}
}
Ok(())
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
djls-django = { workspace = true }
djls-python = { workspace = true }
anyhow = { workspace = true }
@ -12,3 +13,4 @@ serde_json = { workspace = true }
tokio = { version = "1.42.0", features = ["full"] }
tower-lsp = { version = "0.20.0", features = ["proposed"] }
lsp-types = "0.97.0"

View file

@ -1,57 +1,56 @@
mod notifier;
mod server;
use crate::notifier::TowerLspNotifier;
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};
use anyhow::Result;
use djls_django::DjangoProject;
use tower_lsp::jsonrpc::Result as LspResult;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer, LspService, Server};
use tower_lsp::{LanguageServer, LspService, Server};
use djls_python::Python;
#[derive(Debug)]
struct Backend {
client: Client,
python: Python,
struct TowerLspBackend {
server: DjangoLanguageServer,
}
#[tower_lsp::async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, _params: InitializeParams) -> LspResult<InitializeResult> {
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::INCREMENTAL,
)),
..Default::default()
},
offset_encoding: None,
server_info: Some(ServerInfo {
name: String::from("Django Language Server"),
version: Some(String::from("0.1.0")),
}),
})
impl LanguageServer for TowerLspBackend {
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
self.server
.handle_request(LspRequest::Initialize(params))
.map_err(|_| tower_lsp::jsonrpc::Error::internal_error())
}
async fn initialized(&self, _: InitializedParams) {
self.client
.log_message(MessageType::INFO, "server initialized!")
.await;
self.client
.log_message(MessageType::INFO, format!("\n{}", self.python))
.await;
async fn initialized(&self, params: InitializedParams) {
if self
.server
.handle_notification(LspNotification::Initialized(params))
.is_err()
{
// Handle error
}
}
async fn shutdown(&self) -> LspResult<()> {
Ok(())
self.server
.handle_notification(LspNotification::Shutdown)
.map_err(|_| tower_lsp::jsonrpc::Error::internal_error())
}
}
#[tokio::main]
async fn main() -> Result<()> {
let python = Python::initialize()?;
let django = DjangoProject::setup()?;
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let (service, socket) = LspService::build(|client| Backend { client, python }).finish();
let (service, socket) = LspService::build(|client| {
let notifier = Box::new(TowerLspNotifier::new(client.clone()));
let server = DjangoLanguageServer::new(django, notifier);
TowerLspBackend { server }
})
.finish();
Server::new(stdin, stdout, socket).serve(service).await;

View file

@ -0,0 +1,59 @@
use anyhow::Result;
use tower_lsp::async_trait;
use tower_lsp::lsp_types::MessageActionItem;
use tower_lsp::lsp_types::MessageType;
use tower_lsp::Client;
#[async_trait]
pub trait Notifier: Send + Sync {
fn log_message(&self, typ: MessageType, msg: &str) -> Result<()>;
fn show_message(&self, typ: MessageType, msg: &str) -> Result<()>;
async fn show_message_request(
&self,
typ: MessageType,
msg: &str,
actions: Option<Vec<MessageActionItem>>,
) -> Result<Option<MessageActionItem>>;
}
pub struct TowerLspNotifier {
client: Client,
}
impl TowerLspNotifier {
pub fn new(client: Client) -> Self {
Self { client }
}
}
#[async_trait]
impl Notifier for TowerLspNotifier {
fn log_message(&self, typ: MessageType, msg: &str) -> Result<()> {
let client = self.client.clone();
let msg = msg.to_string();
tokio::spawn(async move {
client.log_message(typ, msg).await;
});
Ok(())
}
fn show_message(&self, typ: MessageType, msg: &str) -> Result<()> {
let client = self.client.clone();
let msg = msg.to_string();
tokio::spawn(async move {
client.show_message(typ, msg).await;
});
Ok(())
}
async fn show_message_request(
&self,
typ: MessageType,
msg: &str,
actions: Option<Vec<MessageActionItem>>,
) -> Result<Option<MessageActionItem>> {
let client = self.client.clone();
let msg = msg.to_string();
Ok(client.show_message_request(typ, msg, actions).await?)
}
}

60
crates/djls/src/server.rs Normal file
View file

@ -0,0 +1,60 @@
use crate::notifier::Notifier;
use anyhow::Result;
use djls_django::DjangoProject;
use tower_lsp::lsp_types::*;
const SERVER_NAME: &str = "Django Language Server";
const SERVER_VERSION: &str = "0.1.0";
pub enum LspRequest {
Initialize(InitializeParams),
}
pub enum LspNotification {
Initialized(InitializedParams),
Shutdown,
}
pub struct DjangoLanguageServer {
django: DjangoProject,
notifier: Box<dyn Notifier>,
}
impl DjangoLanguageServer {
pub fn new(django: DjangoProject, notifier: Box<dyn Notifier>) -> Self {
Self { django, notifier }
}
pub fn handle_request(&self, request: LspRequest) -> Result<InitializeResult> {
match request {
LspRequest::Initialize(_params) => Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::INCREMENTAL,
)),
..Default::default()
},
offset_encoding: None,
server_info: Some(ServerInfo {
name: SERVER_NAME.to_string(),
version: Some(SERVER_VERSION.to_string()),
}),
}),
}
}
pub fn handle_notification(&self, notification: LspNotification) -> Result<()> {
match notification {
LspNotification::Initialized(_) => {
self.notifier
.log_message(MessageType::INFO, "server initialized!")?;
self.notifier
.log_message(MessageType::INFO, &format!("\n{}", self.django.py()))?;
self.notifier
.log_message(MessageType::INFO, &format!("\n{}", self.django))?;
Ok(())
}
LspNotification::Shutdown => Ok(()),
}
}
}

90
pyproject.toml Normal file
View file

@ -0,0 +1,90 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "djls"
version = "0.1.0"
description = "Utility Python scripts for Django Language Server"
readme = "README.md"
authors = [
{ name = "Josh Thomas", email = "josh@joshthomas.dev" }
]
requires-python = ">=3.9"
dependencies = [
"django>=4.2",
]
[tool.hatch.build.targets.wheel]
include = ["python/djls"]
[dependency-groups]
dev = [
"django-stubs>=5.1.1",
"ruff>=0.8.2",
]
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".github",
".hg",
".mypy_cache",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"build",
"dist",
"migrations",
"node_modules",
"venv"
]
extend-include = ["*.pyi?"]
indent-width = 4
# Same as Black.
line-length = 88
# Assume Python 3.9
target-version = "py39"
[tool.ruff.format]
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Like Black, use double quotes for strings.
quote-style = "double"
[tool.ruff.lint]
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["A", "B", "C", "D", "E", "F", "I"]
ignore = ["E501", "E741"] # temporary
select = [
"B", # flake8-bugbear
"E", # Pycodestyle
"F", # Pyflakes
"I", # isort
"UP" # pyupgrade
]
unfixable = []
[tool.ruff.lint.isort]
force-single-line = true
known-first-party = ["djls"]
required-imports = ["from __future__ import annotations"]
[tool.ruff.lint.per-file-ignores]
# Tests can use magic values, assertions, and relative imports
"tests/**/*" = ["PLR2004", "S101", "TID252"]
[tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true

0
python/README.md Normal file
View file

0
python/djls/__init__.py Normal file
View file

View file

@ -0,0 +1,31 @@
from __future__ import annotations
import json
from django.conf import settings
from django.template.engine import Engine
def get_django_setup_info():
return {
"installed_apps": list(settings.INSTALLED_APPS),
"templatetags": [
{
"name": tag_name,
"library": module_name.split(".")[-1],
"doc": tag_func.__doc__ if hasattr(tag_func, "__doc__") else None,
}
for module_name, library in (
[("", lib) for lib in Engine.get_default().template_builtins]
+ sorted(Engine.get_default().template_libraries.items())
)
for tag_name, tag_func in library.tags.items()
],
}
if __name__ == "__main__":
import django
django.setup()
print(json.dumps(get_django_setup_info()))

21
python/djls/has_import.py Normal file
View file

@ -0,0 +1,21 @@
# has_import.py
from __future__ import annotations
import json
import sys
def check_import(module: str) -> bool:
try:
module_parts = module.split(".")
current = __import__(module_parts[0])
for part in module_parts[1:]:
current = getattr(current, part)
return True
except (ImportError, AttributeError):
return False
if __name__ == "__main__":
result = {"can_import": check_import(sys.argv[1])}
print(json.dumps(result))

View file

@ -0,0 +1,9 @@
from __future__ import annotations
import json
import sys
from django.conf import settings
if __name__ == "__main__":
print(json.dumps({"has_app": sys.argv[1] in settings.INSTALLED_APPS}))

0
python/djls/py.typed Normal file
View file

View file

@ -0,0 +1,78 @@
from __future__ import annotations
import importlib.metadata
import json
import sys
import sysconfig
from typing import Dict
from typing import List
from typing import Optional
from typing import TypedDict
def get_version_info():
version_parts = sys.version.split()[0].split(".")
patch_and_suffix = version_parts[2]
for i, c in enumerate(patch_and_suffix):
if not c.isdigit():
patch = patch_and_suffix[:i]
suffix = patch_and_suffix[i:]
break
else:
patch = patch_and_suffix
suffix = None
return {
"major": int(version_parts[0]),
"minor": int(version_parts[1]),
"patch": int(patch),
"suffix": suffix,
}
class Package(TypedDict):
name: str
version: str
location: Optional[str]
def get_installed_packages() -> Dict[str, Package]:
packages: Dict[str, Package] = {}
for dist in importlib.metadata.distributions():
try:
location_path = dist.locate_file("")
location = location_path.parent.as_posix() if location_path else None
packages[dist.metadata["Name"]] = {
"name": dist.metadata["Name"],
"version": dist.version,
"location": location,
}
except Exception:
continue
return packages
def get_python_info() -> (
Dict[
str,
str
| Dict[str, str]
| List[str]
| Dict[str, Package]
| Dict[str, int | str | None],
]
):
return {
"version_info": get_version_info(),
"sysconfig_paths": sysconfig.get_paths(),
"sys_prefix": sys.prefix,
"sys_base_prefix": sys.base_prefix,
"sys_executable": sys.executable,
"sys_path": [p for p in sys.path if p],
"packages": get_installed_packages(),
}
if __name__ == "__main__":
print(json.dumps(get_python_info()))

181
uv.lock generated Normal file
View file

@ -0,0 +1,181 @@
version = 1
requires-python = ">=3.9"
[[package]]
name = "asgiref"
version = "3.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 },
]
[[package]]
name = "django"
version = "4.2.17"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/58/709978ddf7e9393c0a89b57a5edbd764ee76eeea68697af3f77f3820980b/Django-4.2.17.tar.gz", hash = "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc", size = 10437674 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/85/457360cb3de496382e35db4c2af054066df5c40e26df31400d0109a0500c/Django-4.2.17-py3-none-any.whl", hash = "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0", size = 7993390 },
]
[[package]]
name = "django-stubs"
version = "5.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
{ name = "django-stubs-ext" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "types-pyyaml" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bf/60/1ae90eb6e2e107bc64a3de9de78a5add7f3b85e491113504eed38d6d2c63/django_stubs-5.1.1.tar.gz", hash = "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b", size = 265624 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/c8/3081d5f994351248fcd60f9aab10cb2020bdd7df0f14e80854373e15d7d4/django_stubs-5.1.1-py3-none-any.whl", hash = "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac", size = 470790 },
]
[[package]]
name = "django-stubs-ext"
version = "5.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ca/62/a7129909d3c94eac957c02eeb05ac57cbca81db4f3f6270a8503697f376a/django_stubs_ext-5.1.1.tar.gz", hash = "sha256:db7364e4f50ae7e5360993dbd58a3a57ea4b2e7e5bab0fbd525ccdb3e7975d1c", size = 9455 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/ed/f79ae5ad993bdf900d61892d2a9fc0145441a507a7579890fb8e21e4a7bc/django_stubs_ext-5.1.1-py3-none-any.whl", hash = "sha256:3907f99e178c93323e2ce908aef8352adb8c047605161f8d9e5e7b4efb5a6a9c", size = 8965 },
]
[[package]]
name = "djls"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "django" },
]
[package.dev-dependencies]
dev = [
{ name = "django-stubs" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [{ name = "django", specifier = ">=4.2" }]
[package.metadata.requires-dev]
dev = [
{ name = "django-stubs", specifier = ">=5.1.1" },
{ name = "ruff", specifier = ">=0.8.2" },
]
[[package]]
name = "ruff"
version = "0.8.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/2b/01245f4f3a727d60bebeacd7ee6d22586c7f62380a2597ddb22c2f45d018/ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5", size = 3349020 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/29/366be70216dba1731a00a41f2f030822b0c96c7c4f3b2c0cdce15cbace74/ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d", size = 10530649 },
{ url = "https://files.pythonhosted.org/packages/63/82/a733956540bb388f00df5a3e6a02467b16c0e529132625fe44ce4c5fb9c7/ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5", size = 10274069 },
{ url = "https://files.pythonhosted.org/packages/3d/12/0b3aa14d1d71546c988a28e1b412981c1b80c8a1072e977a2f30c595cc4a/ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c", size = 9909400 },
{ url = "https://files.pythonhosted.org/packages/23/08/f9f08cefb7921784c891c4151cce6ed357ff49e84b84978440cffbc87408/ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f", size = 10766782 },
{ url = "https://files.pythonhosted.org/packages/e4/71/bf50c321ec179aa420c8ec40adac5ae9cc408d4d37283a485b19a2331ceb/ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897", size = 10286316 },
{ url = "https://files.pythonhosted.org/packages/f2/83/c82688a2a6117539aea0ce63fdf6c08e60fe0202779361223bcd7f40bd74/ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58", size = 11338270 },
{ url = "https://files.pythonhosted.org/packages/7f/d7/bc6a45e5a22e627640388e703160afb1d77c572b1d0fda8b4349f334fc66/ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29", size = 12058579 },
{ url = "https://files.pythonhosted.org/packages/da/3b/64150c93946ec851e6f1707ff586bb460ca671581380c919698d6a9267dc/ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248", size = 11615172 },
{ url = "https://files.pythonhosted.org/packages/e4/9e/cf12b697ea83cfe92ec4509ae414dc4c9b38179cc681a497031f0d0d9a8e/ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93", size = 12882398 },
{ url = "https://files.pythonhosted.org/packages/a9/27/96d10863accf76a9c97baceac30b0a52d917eb985a8ac058bd4636aeede0/ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d", size = 11176094 },
{ url = "https://files.pythonhosted.org/packages/eb/10/cd2fd77d4a4e7f03c29351be0f53278a393186b540b99df68beb5304fddd/ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0", size = 10771884 },
{ url = "https://files.pythonhosted.org/packages/71/5d/beabb2ff18870fc4add05fa3a69a4cb1b1d2d6f83f3cf3ae5ab0d52f455d/ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa", size = 10382535 },
{ url = "https://files.pythonhosted.org/packages/ae/29/6b3fdf3ad3e35b28d87c25a9ff4c8222ad72485ab783936b2b267250d7a7/ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f", size = 10886995 },
{ url = "https://files.pythonhosted.org/packages/e9/dc/859d889b4d9356a1a2cdbc1e4a0dda94052bc5b5300098647e51a58c430b/ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22", size = 11220750 },
{ url = "https://files.pythonhosted.org/packages/0b/08/e8f519f61f1d624264bfd6b8829e4c5f31c3c61193bc3cff1f19dbe7626a/ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1", size = 8729396 },
{ url = "https://files.pythonhosted.org/packages/f8/d4/ba1c7ab72aba37a2b71fe48ab95b80546dbad7a7f35ea28cf66fc5cea5f6/ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea", size = 9594729 },
{ url = "https://files.pythonhosted.org/packages/23/34/db20e12d3db11b8a2a8874258f0f6d96a9a4d631659d54575840557164c8/ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8", size = 9035131 },
]
[[package]]
name = "sqlparse"
version = "0.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/61/5bc3aff85dc5bf98291b37cf469dab74b3d0aef2dd88eade9070a200af05/sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f", size = 84951 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/13/5f6654c9d915077fae255686ca6fa42095b62b7337e3e1aa9e82caa6f43a/sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e", size = 44407 },
]
[[package]]
name = "tomli"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20240917"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/92/7d/a95df0a11f95c8f48d7683f03e4aed1a2c0fc73e9de15cca4d38034bea1a/types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587", size = 12381 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/2c/c1d81d680997d24b0542aa336f0a65bd7835e5224b7670f33a7d617da379/types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570", size = 15264 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "tzdata"
version = "2024.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 },
]