mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-07-07 20:55:02 +00:00
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:
parent
b7a1de98dd
commit
fce343f44d
32 changed files with 1139 additions and 291 deletions
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[env]
|
||||
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
|
@ -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
2
.rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Item"
|
||||
unstable_features = true
|
|
@ -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
12
Justfile
Normal 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
|
1
LICENSE
1
LICENSE
|
@ -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.
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# django-language-server
|
||||
|
||||
A Language Server Protocol implementation for Django projects.
|
||||
|
||||
|
|
12
crates/djls-django/Cargo.toml
Normal file
12
crates/djls-django/Cargo.toml
Normal 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"
|
64
crates/djls-django/src/apps.rs
Normal file
64
crates/djls-django/src/apps.rs
Normal 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(())
|
||||
}
|
||||
}
|
120
crates/djls-django/src/django.rs
Normal file
120
crates/djls-django/src/django.rs
Normal 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),
|
||||
}
|
26
crates/djls-django/src/gis.rs
Normal file
26
crates/djls-django/src/gis.rs
Normal 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),
|
||||
}
|
7
crates/djls-django/src/lib.rs
Normal file
7
crates/djls-django/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod apps;
|
||||
mod django;
|
||||
mod gis;
|
||||
mod scripts;
|
||||
mod templates;
|
||||
|
||||
pub use django::DjangoProject;
|
4
crates/djls-django/src/scripts.rs
Normal file
4
crates/djls-django/src/scripts.rs
Normal 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");
|
30
crates/djls-django/src/templates.rs
Normal file
30
crates/djls-django/src/templates.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
179
crates/djls-python/src/runner.rs
Normal file
179
crates/djls-python/src/runner.rs
Normal 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),
|
||||
}
|
14
crates/djls-python/src/scripts.rs
Normal file
14
crates/djls-python/src/scripts.rs
Normal 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"];
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
59
crates/djls/src/notifier.rs
Normal file
59
crates/djls/src/notifier.rs
Normal 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
60
crates/djls/src/server.rs
Normal 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
90
pyproject.toml
Normal 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
0
python/README.md
Normal file
0
python/djls/__init__.py
Normal file
0
python/djls/__init__.py
Normal file
31
python/djls/django_setup.py
Normal file
31
python/djls/django_setup.py
Normal 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
21
python/djls/has_import.py
Normal 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))
|
9
python/djls/installed_apps_check.py
Normal file
9
python/djls/installed_apps_check.py
Normal 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
0
python/djls/py.typed
Normal file
78
python/djls/python_setup.py
Normal file
78
python/djls/python_setup.py
Normal 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
181
uv.lock
generated
Normal 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 },
|
||||
]
|
Loading…
Add table
Add a link
Reference in a new issue