11import click
2+ import os
3+ import shlex
4+ import shutil
5+ import subprocess
6+ from typing import cast
27
38from tutor import config as tutor_config
4- from tutor import fmt
59from tutor .commands .context import Context
10+ from tutor .commands .config import save as config_save_command
11+
12+
13+ HERE = os .path .abspath (os .path .dirname (__file__ ))
14+ DEPS_ZIP_PATH = os .path .join (HERE , "deps.zip" )
15+ DEPS_PATH = os .path .join (HERE , "deps/" )
616
717
818@click .group (
919 name = "packages" ,
1020 short_help = "Manage packages" ,
1121)
1222def packages_command () -> None :
13- """ """
23+ """Custom persistent package manager for Tutor. """
1424
1525
1626@click .command (name = "list" )
@@ -21,10 +31,103 @@ def list_command(context: Context) -> None:
2131
2232 Entries will be fetched from the `PERSISTENT_PIP_PACKAGES` config setting.
2333 """
34+ config = tutor_config .load (context .root )
35+ packages = [
36+ package for package in cast (list [str ], config ["PERSISTENT_PIP_PACKAGES" ])
37+ ]
38+ print (packages )
2439
40+
41+ @click .command (name = "build" )
42+ @click .pass_obj
43+ def build (context : Context ) -> None :
44+ """Rebuild dependencies from scratch."""
2545 config = tutor_config .load (context .root )
26- packages = [package for package in config ["PERSISTENT_PIP_PACKAGES" ]]
27- fmt .echo (sorted (packages ))
46+ DEPS = cast (list [str ], config ["PERSISTENT_PIP_PACKAGES" ])
47+
48+ build_dir = f"{ context .root } /data/persistent-python-packages"
49+
50+ lib_path = os .path .join (build_dir , "lib" )
51+ bin_path = os .path .join (build_dir , "bin" )
52+
53+ if os .path .exists (lib_path ):
54+ shutil .rmtree (lib_path )
55+ if os .path .exists (bin_path ):
56+ shutil .rmtree (bin_path )
57+
58+ _pip_install (DEPS , build_dir )
59+
60+
61+ @click .command (name = "append" )
62+ @click .pass_obj
63+ @click .pass_context
64+ @click .argument ("package" )
65+ def append (click_context : click .Context , context : Context , package : str ) -> None :
66+ """Append a new package to the list."""
67+ click_context .invoke (
68+ config_save_command ,
69+ interactive = False ,
70+ set_vars = [],
71+ append_vars = [("PERSISTENT_PIP_PACKAGES" , package )],
72+ remove_vars = [],
73+ unset_vars = [],
74+ env_only = False ,
75+ clean_env = False ,
76+ )
77+
78+ build_dir = f"{ context .root } /data/persistent-python-packages"
79+
80+ if os .path .exists (DEPS_ZIP_PATH ):
81+ shutil .unpack_archive (DEPS_ZIP_PATH , extract_dir = build_dir )
82+
83+ _pip_install ([package ], build_dir )
84+
85+
86+ @click .command (name = "remove" )
87+ @click .pass_context
88+ @click .argument ("package" )
89+ def remove (context : click .Context , package : str ) -> None :
90+ """Remove a package and rebuild archive from scratch."""
91+ context .invoke (
92+ config_save_command ,
93+ interactive = False ,
94+ set_vars = [],
95+ append_vars = [],
96+ remove_vars = [("PERSISTENT_PIP_PACKAGES" , package )],
97+ unset_vars = [],
98+ env_only = False ,
99+ clean_env = False ,
100+ )
101+
102+ context .invoke (build )
103+
104+
105+ def _pip_install (deps : list [str ], prefix_dir : str ) -> None :
106+ for dep in deps :
107+ # We use python3.11 because that's whats used in the Dockerfile
108+ check_call (
109+ "python3.11" ,
110+ "-m" ,
111+ "pip" ,
112+ "install" ,
113+ "--no-deps" ,
114+ f"--prefix={ prefix_dir } " ,
115+ dep ,
116+ )
117+ check_call (f'touch "{ prefix_dir } /.uwsgi_trigger"' , shell = True )
118+
119+
120+ def check_call (* args : str , shell : bool = False ) -> None :
121+ if shell :
122+ command = " " .join (args )
123+ print (command )
124+ subprocess .check_call (command , shell = True )
125+ else :
126+ print (shlex .join (args ))
127+ subprocess .check_call (args )
28128
29129
30130packages_command .add_command (list_command )
131+ packages_command .add_command (build )
132+ packages_command .add_command (append )
133+ packages_command .add_command (remove )
0 commit comments