Much commit.

switch to typescript, actual analysis, etc.
master
Guilian Celin--Davanture 2024-04-10 17:55:57 +02:00
parent bc1e4ab83a
commit 5b33c5f8d5
Signed by: Guilian
GPG Key ID: C063AC8F4FC34489
16 changed files with 1352 additions and 222 deletions

BIN
bun.lockb Normal file → Executable file

Binary file not shown.

View File

@ -1,73 +0,0 @@
import express from "express";
import {Liquid} from 'liquidjs';
import path from "path";
const liquidEngine = new Liquid({root: path.resolve(__dirname, 'views/'), jsTruthy: true});
const app = express();
app.use(express.json()); // to support JSON-encoded bodies
app.use(express.urlencoded()); // to support URL-encoded bodies
app.engine('liquid', liquidEngine.express());
app.set('views', [path.join(__dirname, 'views')]);
app.set('view engine', 'liquid');
app.use('/static', express.static('static'));
app.get('/', (req, res) => {
res.render('index');
})
function extractInfoFromPythonDepString(depstring) {
const info = {
name: "",
version_operator: "",
version: "",
}
const parts = depstring.split(';');
const namever = parts[0];
// https://stackoverflow.com/questions/24470567/what-are-the-requirements-for-naming-python-modules
const match = namever.match(/^(?<name>[a-zA-Z_][a-zA-Z_0-9]*)(?<operator>.=)(?<version>[0-9.])/);
if(match !== null && match.groups !== undefined) {
info.name = match.groups['name'];
info.operator = match.groups['operator'];
info.version = match.groups['version'];
}
return info;
}
app.post('/api/dependency', async (req, res) => {
const packagename = req.body.package;
console.log(packagename)
const pypires = await fetch(`https://pypi.org/pypi/${packagename}/json`);
const json = await pypires.json();
const name = json.info.name;
const summary = json.info.summary;
const deps = json.info.requires_dist.map( e => extractInfoFromPythonDepString(e) );
const versions = json.releases;
const latestVersionNumber = Object.entries(versions).sort( ([k1, v1], [k2, v2]) => k2 - v2 )[0];
const latestVersion = versions[latestVersionNumber];
const size = latestVersion.sort((a, b) => b.size - a.size )[0].size;
res.render('dependency', {
name: name,
summary: summary,
weight: size,
dependencies: deps
})
});
app.listen(8080)

552
index.ts Normal file
View File

@ -0,0 +1,552 @@
import path from 'node:path';
import express from 'express';
import {Liquid} from 'liquidjs';
import {RateLimit} from 'async-sema';
import fileUpload from 'express-fileupload';
import Graph from 'graphology';
import forceAtlas2 from 'graphology-layout-forceatlas2';
import circular from 'graphology-layout/circular';
import {assignLayout} from 'graphology-layout/utils';
import {singleSource, singleSourceLength} from 'graphology-shortest-path/unweighted';
import Pep440Version, {Pep440VersionRule} from './pep440version.ts';
import {version} from 'bun';
const liquidEngine = new Liquid({root: path.resolve(__dirname, 'views/'), jsTruthy: true});
const app = express();
app.use(express.json()); // To support JSON-encoded bodies
app.use(express.urlencoded()); // To support URL-encoded bodies
app.use(fileUpload({
limits: { fileSize: 50 * 1024 * 1024 },
}));
app.engine('liquid', liquidEngine.express());
app.set('views', [path.join(__dirname, 'views')]);
app.set('view engine', 'liquid');
app.use('/static', express.static('static'));
app.get('/', (request, res) => {
res.render('index');
});
declare class RateLimit {
constructor(max: number);
}
const rateLimit: any = new RateLimit(5);
function extractInfoFromPythonDepString(depstring: string) {
const parts = depstring.split(';');
const namever = parts[0];
// https://stackoverflow.com/questions/24470567/what-are-the-requirements-for-naming-python-modules
const match = namever.match(/^(?<name>[a-zA-Z_\-][\w\-]*)((?<operator>.=)(?<version>.+))?/);
if (match !== null && match.groups !== undefined) {
let rule;
if( match.groups.version ) {
const version = new Pep440Version(match.groups.version);
rule = new Pep440VersionRule(
version,
match.groups.operator
)
}
else {
rule = Pep440VersionRule.getRuleMatchingAnyVersion()
}
return {
name: match.groups.name,
rule: rule
};
}
else {
return undefined;
}
}
async function getRawPackageInfo(packagename: string) {
await rateLimit();
const pypires = await fetch(`https://pypi.org/pypi/${packagename}/json`);
const json = await pypires.json();
return json;
}
interface PipReleaseFile {
size: number;
packagetype: string;
upload_time_iso_8601: string;
[key: string]: any;
}
async function getFullPackageInfo(packagename: string, rule: Pep440VersionRule = Pep440VersionRule.getRuleMatchingAnyVersion(), fromGraph = new Graph()) {
const dependencyGraph = fromGraph;
// Analyse the requested package
const packageJson = await getRawPackageInfo(packagename);
let longestDependencyChain = 0;
//const dependenciesList = packageJson.info.requires_dist ?? [];
const recurseDepsPackageInfo = async (packagename: string, rule: Pep440VersionRule = Pep440VersionRule.getRuleMatchingAnyVersion(), descPath: string = "") => {
console.log(`[Recurse] Going to scan package ${packagename}`);
if( descPath == "" ) {
descPath = packagename;
}
else {
descPath += `/${packagename}`
}
if( dependencyGraph.hasNode(packagename) ) {
// figure out if the current path taken to this package is shorter than the one in the graph
// have to add the current package name manually as it isn't added yet
const paramPathParts: string[] = descPath.split('/');
const graphPathParts: string[] = dependencyGraph.getNodeAttribute(packagename, 'path').split('/');
if( paramPathParts.length < graphPathParts.length ) {
// if so, change the graph path to the current one
dependencyGraph.setNodeAttribute(packagename, 'path', descPath)
console.log(`[Recurse] Found better way of getting to ${packagename}: ${descPath}`);
}
const shortestDist = Math.min( paramPathParts.length, graphPathParts.length );
if( longestDependencyChain < shortestDist ) {
longestDependencyChain = shortestDist;
}
console.log(`[Recurse] Already scanned ${packagename} ; skipping.`);
return;
}
console.log(`[Recurse] Scanning package ${packagename}`);
const json = await getRawPackageInfo(packagename);
// ==================================================
// 1. get biggest file of highest possible version
// TODO: don't choose biggest file, choose in a smart way (like asking the user's OS and arch)
const releasesEntries = Object.entries(json.releases as {[key: string]: PipReleaseFile[]});
const releases: {[key: string]: PipReleaseFile[]} = {};
for( const [version, data] of releasesEntries ) {
try {
releases[new Pep440Version(version).releaseString()] = data;
}
catch {
console.log(`Failed to parse version: ${version}`);
}
}
const versions = Object.keys(releases).map( v => {
console.log(`[Recurse] Parsing version ${v} for ${packagename}`);
return new Pep440Version(v);
});
const highestVersions = rule.getSortedMatchingVersions(versions);
if( highestVersions.length === 0 ) {
throw new Error("MERDE!");
}
let highestVersion: undefined | Pep440Version = undefined;
for( const version of highestVersions ) {
if( releases[version.releaseString()].length !== 0 ) {
highestVersion = version;
break;
}
}
if( highestVersion === undefined ) {
throw new Error("MERDE!");
}
const highestRelease = releases[highestVersion.releaseString()];
const biggestFile = highestRelease.sort( (a, b) => b.size - a.size )[0];
console.log(`[Recurse] ${packagename} has highest version ${highestVersion.releaseString()}`);
// ==================================================
// 2. add to the graph
const summary = json.summary;
dependencyGraph.addNode(packagename, {
label: packagename,
size: 10,
summary: summary,
version: highestVersion.releaseString(),
weight: biggestFile.size,
path: descPath
});
/////////////////
const dependenciesList = packageJson.info.requires_dist ?? [];
console.log(`[Recurse] ${packagename} has deps:\n - ${dependenciesList.join('\n - ')}`);
for( const depString of dependenciesList ) {
console.log(`[Recurse] ${packagename} depends on: ${depString}`);
const info = extractInfoFromPythonDepString(depString);
if( info === undefined ) continue;
await recurseDepsPackageInfo(info.name, info.rule, descPath);
if( dependencyGraph.hasEdge(packagename, info.name) === false ) {
dependencyGraph.addEdge(packagename, info.name);
}
}
}
await recurseDepsPackageInfo(packagename, rule);
console.log(`Longest dependency chain is ${longestDependencyChain}`);
//const paths = singleSource(dependencyGraph, packagename)
const depthColors = [
"#D14D41",
"#DA702C",
"#D0A215",
"#879A39",
"#3AA99F",
"#4385BE",
"#8B7EC8",
"#CE5D97",
]
const lengthmap = singleSourceLength(dependencyGraph, packagename);
const longestPathLength = Math.max(...Object.values(lengthmap))
for( const [node, dist] of Object.entries(lengthmap) ) {
dependencyGraph.setNodeAttribute(node, 'color', depthColors[dist + 1] )
dependencyGraph.setNodeAttribute(node, 'actual_depth', dist )
}
dependencyGraph.forEachEdge((edge, attrs, source, target, sourceAttrs, targetAttrs) => {
const sourceDepth = sourceAttrs.actual_depth; //.path.split('/').length - 1;
const targetDepth = targetAttrs.actual_depth; //.path.split('/').length - 1;
if( sourceDepth >= targetDepth ) {
dependencyGraph.setEdgeAttribute( edge, "color", "#B7B5AC")
dependencyGraph.setEdgeAttribute( edge, "size", 1)
dependencyGraph.setEdgeAttribute( edge, "weight", 1)
}
else {
dependencyGraph.setEdgeAttribute( edge, "color", depthColors[ sourceDepth ])
dependencyGraph.setEdgeAttribute( edge, "size", 3 + 3 * (longestPathLength - sourceDepth))
dependencyGraph.setEdgeAttribute( edge, "weight", 3 + 3 * (longestPathLength - sourceDepth))
}
dependencyGraph.setEdgeAttribute( edge, "type", "arrow");
});
//circular.assign(dependencyGraph, {scale: 100});
//forceAtlas2.assign(dependencyGraph, 100)
dependencyGraph.setNodeAttribute(packagename, 'size', 20)
//dependencyGraph.setNodeAttribute(packagename, 'x', 0)
//dependencyGraph.setNodeAttribute(packagename, 'y', 0)
circular.assign(dependencyGraph, {scale: 100});
const positions = forceAtlas2(dependencyGraph, {
iterations: 50,
settings: {
edgeWeightInfluence: 1,
...forceAtlas2.inferSettings(dependencyGraph),
}
});
assignLayout(dependencyGraph, positions);
return dependencyGraph;
}
app.get('/graph/:packagename', (req, res) => {
const packagename = req.params.packagename;
res.render('graph', {
packagename: packagename,
})
});
app.get('/api/graph/:packagename', async (req, res) => {
const packagename = req.params.packagename;
const depGraph = await getFullPackageInfo(packagename);
const serializedDepGraph = depGraph.export();
let totalDownloadSize = 0;
depGraph.forEachNode((node, attrs) => {
totalDownloadSize += attrs.weight;
})
const units = [
'b',
'kb',
'Mb',
'Gb'
]
let currentUnit = 0;
while( totalDownloadSize > 1000 ) {
totalDownloadSize = totalDownloadSize / 1000;
currentUnit++;
}
const unit = units[currentUnit];
res.json({
weight: totalDownloadSize,
weightUnit: unit,
graph: serializedDepGraph
});
});
app.post('/api/upload-requirements', async (req, res) => {
// get the uploaded requirements.txt file, from the input with the name "file"
const files = req.files;
if( files === undefined || files === null ) {
res.status(400);
res.send("No file uploaded");
return;
}
const file = files.requirementsFile;
if( file === undefined || file === null ) {
res.status(400);
res.send("No file uploaded");
return;
}
if( Array.isArray(file) ) {
res.status(400);
res.send("Don't send multiple files.");
return;
}
// convert to string
const content = '' + file.data;
const infos = content.replaceAll('\r\n', '\n').split('\n').map(extractInfoFromPythonDepString);
let dependencyGraph = new Graph();
for( const info of infos ) {
if( info === undefined ) {
console.log(`Got an undefined info !`)
continue;
}
dependencyGraph = await getFullPackageInfo(info.name, info.rule, dependencyGraph);
}
const serializedDepGraph = dependencyGraph.export();
const getHumanWeight = (weight: number) => {
const units = [
'b',
'kb',
'Mb',
'Gb'
]
let currentUnit = 0;
while( weight > 1000 ) {
weight = weight / 1000;
currentUnit++;
}
return `${Math.round(weight)}${units[currentUnit]}`
}
let nbNodes = 0;
let totalDownloadSize = 0;
dependencyGraph.forEachNode((node, attrs) => {
nbNodes++;
totalDownloadSize += attrs.weight;
})
let packageByWeight: {name: string, weight: number, humanWeight: string}[] = [];
dependencyGraph.forEachNode((node, attrs) => {
packageByWeight.push({
name: node,
weight: attrs.weight,
humanWeight: getHumanWeight(attrs.weight),
})
});
packageByWeight = packageByWeight.sort( (a, b) => b.weight - a.weight);
const humanDownloadSize = getHumanWeight(totalDownloadSize);
res.render('fullgraph', {
//packagename: packagename,
nb_deps: nbNodes,
totalWeight: {
raw: totalDownloadSize,
value: humanDownloadSize,
},
graph: serializedDepGraph,
packageByWeight: packageByWeight
});
})
/**
* Get info about a package
*/ /*
async function getFullPackageInfo(packagename: string, versionRequirement={operator: "", version: ""}, seenBefore: Set<string> = new Set([])) {
const packagejson = await getRawPackageInfo(packagename);
const name = packagejson.info.name;
const summary = packagejson.info.summary;
const dependenciesList = packagejson.info.requires_dist ?? [];
const builtDepsList: Map<string, {[key: string]: string]}>
for( const depString of dependenciesList ) {
const dep = extractInfoFromPythonDepString(depString);
if( seenBefore.has(dep.name) ) {
continue;
}
const json = await getRawPackageInfo(dep.name, dep.version)
}
}*/
/*
app.post('/api/dependency-tree', async (request, res) => {
const packagename = request.body.package;
console.log("Requested package: ", packagename);
const json = await getRawPackageInfo(packagename);
const name = json.info.name;
const summary = json.info.summary;
let deps = json.info.requires_dist ? json.info.requires_dist.map(e => extractInfoFromPythonDepString(e)) : [];
// avoid duplicates (sometimes packages list themselves as a dependency to create "dependency groups" (?))
deps = deps.filter( dep => dep.name !== packagename );
const versions = Object.entries(json.releases).filter( ([k, v]) => v.length > 0 );
// sort by size, preferring wheels
const versionsSorted = versions.map( ([versionNumber, filesArray]) => {
const wheels = filesArray.filter( file => file.packagetype === 'bdist_wheel' );
if( wheels.length > 0 ) {
// sort by size
const biggestWheel = wheels.sort( (a, b) => b.size - a.size )[0];
return [versionNumber, biggestWheel];
}
else {
const biggestFile = filesArray.sort( (a, b) => b.size - a.size )[0];
return [versionNumber, biggestFile];
}
});
//const sorted = filesArray.sort( (a, b) => b.upload_time_iso8601 - a.upload_time_iso8601 );
const [latestVersionNumber, latestVersion] = versions[0];
const sortedFiles = latestVersion.sort((a, b) => b.size - a.size);
const size = sortedFiles[0].size;
res.render('dependency', {
name,
summary,
weight: size,
dependencies: deps,
// DEBUG
has_parent: request.body.isfirst !== "true",
});
});*/
app.listen(8080);

View File

@ -1 +1,27 @@
{"dependencies":{"express":"^4.19.2","liquidjs":"^10.10.2"},"name":"depyth","module":"index.js","type":"module","devDependencies":{"@types/bun":"latest"},"peerDependencies":{"typescript":"^5.0.0"}}
{
"name": "depyth",
"dependencies": {
"async-sema": "^3.1.1",
"express": "^4.19.2",
"express-fileupload": "^1.5.0",
"graphology": "^0.25.4",
"graphology-layout": "^0.6.1",
"graphology-layout-forceatlas2": "^0.10.1",
"graphology-shortest-path": "^2.1.0",
"liquidjs": "^10.10.2",
"melodyc": "^0.19.0"
},
"scripts": {
"css:watch": "npx tailwindcss -i ./input.css -o ./static/styles.css --watch",
"css:watch:prod": "npx tailwindcss -i ./input.css -o ./static/styles.css --watch --minify"
},
"module": "index.js",
"type": "module",
"devDependencies": {
"@types/bun": "latest",
"graphology-types": "^0.24.7"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}

416
pep440version.ts Normal file
View File

@ -0,0 +1,416 @@
import { compiler } from "melodyc";
/*
VERSION_PATTERN = r"""
v?
(?:
(?:(?P<epoch>[0-9]+)!)? # epoch
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
(?P<pre> # pre-release
[-_\.]?
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
[-_\.]?
(?P<pre_n>[0-9]+)?
)?
(?P<post> # post release
(?:-(?P<post_n1>[0-9]+))
|
(?:
[-_\.]?
(?P<post_l>post|rev|r)
[-_\.]?
(?P<post_n2>[0-9]+)?
)
)?
(?P<dev> # dev release
[-_\.]?
(?P<dev_l>dev)
[-_\.]?
(?P<dev_n>[0-9]+)?
)?
)
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
"""
*/
const VERSION_PATTERN = `
<start>;
option of "v";
match {
option of match {
capture epoch {
some of <digit>;
}
"!";
}
capture release {
some of <digit>;
any of match {
".";
some of <digit>;
}
}
option of capture pre {
option of either {
".";
"_";
"-";
}
capture pre_l {
either {
"a";
"b";
"c";
"rc";
"alpha";
"beta";
"pre";
"preview";
}
}
option of either {
".";
"_";
"-";
}
capture pre_n {
some of <digit>;
}
}
option of capture post {
either {
match {
"-";
capture post_n {
some of <digit>;
}
}
match {
option of either {
".";
"_";
"-";
}
capture post_l {
either {
"post";
"rev";
"r";
}
}
option of either {
".";
"_";
"-";
}
option of capture post_n {
some of <digit>;
}
}
}
}
option of capture dev {
option of either {
".";
"_";
"-";
}
capture dev_l {
"dev";
}
option of either {
".";
"_";
"-";
}
option of capture dev_n {
some of <digit>;
}
}
option of match {
"+";
capture local {
some of either {
a to z;
0 to 9;
}
any of match {
option of either {
".";
"_";
"-";
}
some of either {
a to z;
0 to 9;
}
}
}
}
}
// get rid of random bullshit at the end
capture random_bullshit {
any of <char>;
}
<end>;
`;
/*
*/
const output = compiler(VERSION_PATTERN);
console.log(output);
const VERSION_REGEX = new RegExp(output);
export default class Pep440Version {
epoch?: number;
release: {
major: number;
minor: number;
patch: number;
};
preRelease?: {
name: string;
version?: number;
};
postRelease?: {
name?: string;
version?: number;
};
dev?: {
name: string;
version?: number;
};
random_bullshit_at_the_end?: string;
/**
*
* @param {string} versionString
*/
constructor(versionString: string) {
console.log(`Constructing version from ${versionString}`);
const match = versionString.match(VERSION_REGEX);
if( match === null || match.groups === undefined ) {
throw new Error("Couldn't parse version: " + versionString)
}
const g = match.groups;
if( g['release'] === undefined ) {
throw new Error("Couldn't parse version (because of the absence of a release): " + versionString)
}
const [ major, minor, patch ] = g['release'].split(".").map( e => Number(e) );
this.release = {
major: major ?? 0,
minor: minor ?? 0,
patch: patch ?? 0,
}
this.epoch = Number(g['epoch']);
if( g['pre'] ) {
this.preRelease = {
name: g['pre_l'],
version: g['pre_n'] ? Number(g['pre_n']) : undefined,
}
}
if( g['post'] ) {
this.postRelease = {
name: g['post_l'],
version: g['post_n'] ? Number(g['post_n']) : undefined,
}
}
if( g['dev'] ) {
this.dev = {
name: g['dev_l'],
version: g['dev_n'] ? Number(g['dev_n']) : undefined,
}
}
this.random_bullshit_at_the_end = g['random_bullshit'];
}
/**
* @returns the version (as as tring) in a format that can be correctly compared
* even if it looks goofy
*/
toComparableString() {
// comparing strings is annoying
// for example: "1.2" > "1.12", even though 12 > 2
//
// To fix that issue, we're going to have fixed-length numbers,
// so that comparing 1.2 and 1.12 would mean comparing
// "000012" > "000002", this one being correct because 1 > 0.
// pad to 8 because who in their right mind would use 8 digits for the major version
// (noting that some people use 4 because they use years)
const PAD_LENGTH = 8;
const epoch = (this.epoch?.toString() ?? '0').padStart(PAD_LENGTH, '0');
const release_major = (this.release.major.toString() ?? '0').padStart(PAD_LENGTH, '0');
const release_minor = (this.release.minor.toString() ?? '0').padStart(PAD_LENGTH, '0');
const release_patch = (this.release.patch.toString() ?? '0').padStart(PAD_LENGTH, '0');
const release = `${release_major}.${release_minor}.${release_patch}`;
const prename = (this.preRelease?.name || '').padStart(PAD_LENGTH, '0');
const postname = (this.postRelease?.name || '').padStart(PAD_LENGTH, '0');
const devname = (this.dev?.name || '').padStart(PAD_LENGTH, '0');
const prever = (this.preRelease?.version?.toString() || '0').padStart(PAD_LENGTH, '0');
const postver = (this.postRelease?.version?.toString() || '0').padStart(PAD_LENGTH, '0');
const devver = (this.dev?.version?.toString() || '0').padStart(PAD_LENGTH, '0');
return `${epoch}!${release}.${prename}${prever}.${postname}${postver}.${devname}${devver}`;
}
isAbove(other: Pep440Version) {
return this.toComparableString() > other.toComparableString();
}
isBelow(other: Pep440Version) {
return this.toComparableString() < other.toComparableString();
}
isEqual(other: Pep440Version) {
return this.toComparableString() === other.toComparableString();
}
compare(other: Pep440Version) {
if( this.isAbove(other) ) {
return -1;
}
else if( this.isBelow(other) ) {
return 1;
}
else {
return 0;
}
}
/**
* @returns the version as a Pep440-compliant, human-readable string (e.g. "2.1.3")
*/
releaseString() {
let str = "";
if( this.epoch ) {
str += `${this.epoch}!`;
}
str += this.release.major ?? 0;
str += '.';
str += this.release.minor ?? 0;
str += '.';
str += this.release.patch ?? 0;
if(this.preRelease) {
str += '.';
if( this.preRelease.name ) {
str += this.preRelease.name;
}
if( this.preRelease.version ) {
str += this.preRelease.version;
}
}
if(this.postRelease) {
str += '.';
if( this.postRelease.name ) {
str += this.postRelease.name;
}
if( this.postRelease.version ) {
str += this.postRelease.version;
}
}
if(this.dev) {
str += '.';
if( this.dev.name ) {
str += this.dev.name;
}
if( this.dev.version ) {
str += this.dev.version;
}
}
if( this.random_bullshit_at_the_end ) {
str += this.random_bullshit_at_the_end;
}
return str;
}
//static fromString(...versionStrings: string[]) {
// return versionStrings.map( v => new Pep440Version(v) );
//}
}
type OPERATOR_FUNCTION = (source: Pep440Version, target: Pep440Version) => boolean
type OPERATORS_FN_type = {
[key: string]: OPERATOR_FUNCTION;
};
const OPERATORS_FUNCTIONS: OPERATORS_FN_type = {
">=": (source, target) => target.isAbove(source) || target.isEqual(source),
">": (source, target) => target.isAbove(source),
"==": (source, target) => target.isEqual(source),
"": (source, target) => true,
};
export class Pep440VersionRule {
version: Pep440Version;
operator: OPERATOR_FUNCTION;
constructor(version: Pep440Version, operator: string = "") {
this.version = version;
this.operator = OPERATORS_FUNCTIONS[operator];
}
/**
*/
getSortedMatchingVersions(versions: Pep440Version[]) {
const sorted = versions.sort( (a, b) => a.compare(b) );
let filtered;
if( typeof this.operator === "function" ) {
filtered = sorted.filter( v => this.operator( this.version, v ) );
}
else {
filtered = sorted;
}
return filtered;
}
static getRuleMatchingAnyVersion() {
return new Pep440VersionRule(new Pep440Version("0.0.0"))
}
}
//console.log( new Pep440Version("4!1.2.3dev2").toComparableString() )

52
static/bars.svg Normal file
View File

@ -0,0 +1,52 @@
<svg width="16" height="16" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#494949">
<rect y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="30" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="60" width="15" height="140" rx="6">
<animate attributeName="height"
begin="0s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="90" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="120" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

2
static/lib/graphology.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
static/lib/sigma.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@
}
/*
! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
*/
/*
@ -221,6 +221,8 @@ textarea {
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
@ -244,9 +246,9 @@ select {
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
@ -509,6 +511,10 @@ html {
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
@ -559,6 +565,10 @@ html {
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
.container {
@ -595,17 +605,20 @@ html {
}
}
.m-2 {
margin: 0.5rem;
.absolute {
position: absolute;
}
.m-1 {
margin: 0.25rem;
.relative {
position: relative;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
.left-\[calc\(-1rem-1px\)\] {
left: calc(-1rem - 1px);
}
.top-4 {
top: 1rem;
}
.mx-auto {
@ -618,25 +631,27 @@ html {
margin-bottom: 0.25rem;
}
.my-8 {
margin-top: 2rem;
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.mt-1 {
margin-top: 0.25rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.ml-1 {
margin-left: 0.25rem;
.ml-8 {
margin-left: 2rem;
}
.block {
@ -647,10 +662,26 @@ html {
display: flex;
}
.grid {
display: grid;
}
.hidden {
display: none;
}
.h-4 {
height: 1rem;
}
.h-\[600px\] {
height: 600px;
}
.h-\[60vh\] {
height: 60vh;
}
.h-full {
height: 100%;
}
@ -665,6 +696,18 @@ html {
height: min-content;
}
.h-\[80vh\] {
height: 80vh;
}
.h-16 {
height: 4rem;
}
.min-h-72 {
min-height: 18rem;
}
.min-h-max {
min-height: -moz-max-content;
min-height: max-content;
@ -674,6 +717,10 @@ html {
min-height: 100vh;
}
.w-4 {
width: 1rem;
}
.w-fit {
width: -moz-fit-content;
width: fit-content;
@ -683,30 +730,6 @@ html {
width: 100%;
}
.min-w-\[50vw\] {
min-width: 50vw;
}
.min-w-\[80vw\] {
min-width: 80vw;
}
.max-w-\[600px\] {
max-width: 600px;
}
.max-w-\[300px\] {
max-width: 300px;
}
.max-w-\[200px\] {
max-width: 200px;
}
.max-w-\[20rem\] {
max-width: 20rem;
}
.max-w-\[800px\] {
max-width: 800px;
}
@ -715,8 +738,13 @@ html {
flex: 1 1 0%;
}
.list-disc {
list-style-type: disc;
.-translate-y-1\/2 {
--tw-translate-y: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.grid-cols-\[auto_1fr\] {
grid-template-columns: auto 1fr;
}
.flex-col {
@ -731,14 +759,26 @@ html {
justify-content: center;
}
.border-2 {
border-width: 2px;
.gap-y-1 {
row-gap: 0.25rem;
}
.border {
border-width: 1px;
}
.border-2 {
border-width: 2px;
}
.border-4 {
border-width: 4px;
}
.border-l-2 {
border-left-width: 2px;
}
.border-r-0 {
border-right-width: 0px;
}
@ -753,24 +793,19 @@ html {
background-color: rgb(14 116 144 / var(--tw-bg-opacity));
}
.bg-emerald-400 {
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(52 211 153 / var(--tw-bg-opacity));
}
.bg-emerald-300 {
--tw-bg-opacity: 1;
background-color: rgb(110 231 183 / var(--tw-bg-opacity));
}
.p-4 {
padding: 1rem;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.p-2 {
padding: 0.5rem;
}
.p-4 {
padding: 1rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@ -781,51 +816,22 @@ html {
padding-bottom: 0.25rem;
}
.px-8 {
padding-left: 2rem;
padding-right: 2rem;
.pl-2 {
padding-left: 0.5rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.pl-0 {
padding-left: 0px;
}
.pl-4 {
padding-left: 1rem;
.pt-4 {
padding-top: 1rem;
}
.text-center {
text-align: center;
}
.align-middle {
vertical-align: middle;
}
.font-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
@ -836,15 +842,39 @@ html {
line-height: 2.25rem;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.font-bold {
font-weight: 700;
}
.italic {
font-style: italic;
}
.text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));

View File

@ -1,13 +1,22 @@
<div class="p-2 w-full border border-black">
<p class="text-2xl font-mono">{{name}} <span class="text-sm">{{weight}}</span></p>
<div class="w-full flex flex-col">
<div class="relative pl-2">
{% if has_parent %}
<div class="absolute top-4 left-[calc(-1rem-1px)] -translate-y-1/2 w-4 h-4 bg-white border-4 border-black radius-full"></div>
{% endif %}
<span class="text-2xl font-mono">{{name}} <span class="text-sm">{{weight}}</span></span>
<p>{{summary}}</p>
</div>
<ul class="mt-2">
{% if dependencies != empty %}
<ul class="flex flex-col gap-y-1 pl-2 ml-8 border-l-2 border-black pt-4">
{% for dep in dependencies %}
<form class="p-2 w-full border border-black" hx-post="/api/dependency" hx-swap="outerHTML" hx-trigger="load delay:100ms">
<form class="p-2 w-full border border-black" hx-post="/api/dependency-tree" hx-swap="outerHTML" hx-trigger="load">
<input type="hidden" name="package" value="{{dep.name}}"/>
<p class="text-2xl font-mono">{{dep.name}}</p>
</form>
{% endfor %}
</ul>
{% else %}
<p class="pl-2">No dependencies</p>
{% endif %}
</div>

49
views/fullgraph.liquid Normal file
View File

@ -0,0 +1,49 @@
{% layout 'layout.liquid' %}
{% block head %}
<script src="/static/lib/sigma.min.js"></script>
<script src="/static/lib/graphology.umd.min.js"></script>
<title>Depyth</title>
{% endblock %}
{% block content %}
<main class="p-4 flex-1 flex flex-col container mx-auto">
<header class="w-fit mx-auto mb-8">
<h1 class="text-5xl my-4 text-center">Depyth</h1>
</header>
<h2 class="text-4xl my-3">Analysis of your requirements.txt</h2>
<h3 class="text-3xl my-3">General</h3>
<p>Installing your requirements.txt would mean installing up to <span class="font-bold">{{ nb_deps }}</span> packages, for a total download size of around <span class="font-bold">{{totalWeight.value}}</span>.</p>
<p>Note: this estimation does not take into account platforms and architectures, and includes all optional packages.</p>
<h3 class="text-3xl my-3">Weight chart</h3>
<div class="grid grid-cols-[auto_1fr]">
{% for package in packageByWeight %}
<div class="flex flex-col justify-center h-16">
<p class="w-fit text-xl font-bold">{{package.name}}</p>
<p>{{package.humanWeight}} - {{ package.weight | divided_by: totalWeight.raw | times: 100 | round }}%</p>
</div>
<progress value="{{ package.weight }}" max="{{ totalWeight.raw }}" min="0" class="w-full h-16"></progress>
{% endfor %}
</div>
<h3 class="text-3xl my-3">Dependency graph</h3>
<div id="graph" class="w-full h-[60vh] bg-white border border-black"></div>
<script>
const graph = new graphology.Graph();
graph.import( JSON.parse(`{{ graph | json }}`) );
// Instantiate sigma.js and render the graph
const sigmaInstance = new Sigma(graph, document.getElementById("graph"));
</script>
</main>
{% endblock %}

52
views/graph.liquid Normal file
View File

@ -0,0 +1,52 @@
{% layout 'layout.liquid' %}
{% block head %}
<script src="/static/lib/sigma.min.js"></script>
<script src="/static/lib/graphology.umd.min.js"></script>
<title>Depyth</title>
{% endblock %}
{% block content %}
<main class="p-4 mx-auto flex-1 max-w-[800px]">
<header class="w-fit mx-auto mb-8">
<h1 class="text-5xl my-4 text-center">Depyth</h1>
<p class="italic text-center">Dumb python packages things</p>
</header>
<h2 class="text-4xl my-3">{{packagename}}</h2>
<h3 class="text-3xl my-2">General info</h3>
<p>Installing this package means installing up to <span id="total-packages-installed" class="font-bold">0</span> packages</p>
<p>Installing this package means installing up to <span id="total-weight" class="font-bold">0</span><span id="total-weight-unit">kb</span>.</p>
<h3 class="text-3xl my-2">Dependency graph</h3>
<div id="graph" class="w-full h-[600px] bg-white border border-black"></div>
<script>
// Create a graphology graph
fetch("/api/graph/{{packagename}}").then( res => res.json() ).then( json => {
console.log(json);
document.getElementById('total-packages-installed').innerText = '?';
document.getElementById('total-weight').innerText = json.weight;
document.getElementById('total-weight-unit').innerText = json.weightUnit;
const graph = new graphology.Graph();
graph.import( json.graph );
// Instantiate sigma.js and render the graph
const sigmaInstance = new Sigma(graph, document.getElementById("graph"));
})
</script>
</main>
{% endblock %}

View File

@ -1,14 +1,41 @@
{% layout 'layout.liquid' %}
{% block head %}
<script src="/static/lib/sigma.min.js"></script>
<script src="/static/lib/graphology.umd.min.js"></script>
<title>Depyth</title>
{% endblock %}
{% block content %}
<main class="p-4 mx-auto flex-1 max-w-[800px]">
<h1 class="text-5xl my-4">Depyth</h1>
<main class="p-4 mx-auto flex-1 max-w-[800px]" id="main">
<header class="w-fit mx-auto mb-8">
<h1 class="text-5xl my-4 text-center">Depyth</h1>
<p class="italic text-center">Dumb python packages thing</p>
</header>
<form hx-post="/api/dependency" hx-target="#displaybox" hx-swap="innerHTML" class="flex flex-col">
<h2 class="text-4xl my-3">Inspect a requirements.txt file</h2>
<form hx-post="/api/upload-requirements" class="flex flex-col" id="requirements-form" hx-encoding="multipart/form-data" hx-target="#main" hx-swap="outerHTML">
<div class="flex">
<input type='file' name='requirementsFile' class="flex-1 border-2 border-black">
<button class="h-full w-fit px-2 py-1 bg-cyan-700 text-white flex border-2 border-black">
Upload
</button>
</div>
<div class="htmx-indicator flex flex-col w-full min-h-72 items-center justify-center">
<img src="/static/bars.svg" width="64" height="64"/>
<p class="text-center">Analysing your (quite bloated) requirements...</p>
</div>
</form>
{% if false %}
<h2 class="text-4xl my-3">Inspect a package</h2>
<form hx-post="/api/dependency-tree-non-infinite" hx-target="#noinfinite-displaybox" hx-swap="innerHTML" class="flex flex-col">
<input type="hidden" name="isfirst" value="true"/>
<label for="package-name-input" class="block my-1">Enter the name of the package to inspect</label>
<div class="flex h-min w-full">
<input
@ -23,23 +50,10 @@
<button class="h-full w-fit px-2 py-1 bg-cyan-700 text-white flex border-2 border-black">submit</button>
</div>
</form>
{% endif %}
<div class="flex flex-col w-full my-8" id="displaybox">
<!--<div class="p-2 w-full border border-black">
<p class="text-2xl font-mono">torch <span class="text-sm">158kb</span></p>
<p>Tensors and Dynamic neural networks in Python with strong GPU acceleration.</p>
<ul class="mt-2">
<div class="p-2 w-full border border-black">
<p class="text-2xl font-mono">filelock <span class="text-sm">33kb</span></p>
<p>A platform independent file lock.</p>
</div>
</ul>
</div>-->
</div>
</main>
{% endblock %}

View File

@ -9,7 +9,7 @@
<link href="/static/styles.css" rel="stylesheet">
<script src="/static/htmx.min.js" defer></script>
<script src="/static/lib/htmx.min.js" defer></script>
{% block head %}{% endblock %}
</head>